如何在網(wǎng)頁(yè)上高效渲染 1000 萬(wàn)張小圖片的?
最近,看到一個(gè)名為 10MPage.com 的網(wǎng)站,目標(biāo)是記錄 2025 年互聯(lián)網(wǎng)的時(shí)代印記。每個(gè)用戶都可以上傳一張 64x64 像素的小圖片,形成一個(gè)龐大的互聯(lián)網(wǎng)影像檔案。
正如名字所暗示的,這個(gè)頁(yè)面需要承載高達(dá) 1000 萬(wàn)張小圖片。剛開始想到這個(gè)概念時(shí),心想如何高效渲染這些圖片?。在本文中,我將分享作者嘗試的各種方案,以及最終實(shí)現(xiàn)的高效解決方案。
在你繼續(xù)閱讀之前,可以先訪問一下 10MPage.com 看看能不能猜到我是如何實(shí)現(xiàn)的。如果你已經(jīng)打開了10MPage,不妨也為自己上傳一張圖片,搶占一個(gè)位置吧!??
HTML <img> 標(biāo)簽 vs Canvas
首先面臨的選擇是:用傳統(tǒng)的 HTML 元素來(lái)渲染,還是用 Canvas 來(lái)進(jìn)行繪制。
方法一:大量單獨(dú)的 <img> 標(biāo)簽
我最初使用了單獨(dú)的 <img> 標(biāo)簽分別加載圖片。我寫了一個(gè)腳本,生成一個(gè)32x32(共1024張圖片)的圖片網(wǎng)格,用 Laravel Blade 模板進(jìn)行渲染:
<div class="grid" id="grid">
@for($y = 0; $y < 32; $y++)
<div class="row">
@for($x = 0; $x < 32; $x++)
<div class="tile">
<img src="http://10mpage.test/tiles/{{$y}}x{{$x}}.png" alt="Tile {{$y}}x{{$x}}">
</div>
@endfor
</div>
@endfor
</div>
對(duì)應(yīng)的 CSS 樣式:
body {
margin: 0;
padding: 0;
overflow: auto; /* 允許滾動(dòng) */
}
.grid {
display: block;
position: relative;
width: 100%;
}
.row {
display: flex;
}
.tile {
width: 64px;
height: 64px;
box-sizing: border-box;
border: 1px solid #ccc;
}
.tile img {
width: 64px;
height: 64px;
object-fit: cover;
}
這種方式初步看起來(lái)不錯(cuò),但潛藏幾個(gè)嚴(yán)重問題:
- 瀏覽器滾動(dòng)性能差
- DOM 節(jié)點(diǎn)數(shù)量龐大,性能開銷大
- 大量圖片同時(shí)加載,網(wǎng)絡(luò)請(qǐng)求數(shù)激增
- 難以實(shí)現(xiàn)平滑滾動(dòng)或高級(jí)動(dòng)畫效果
方法二:Canvas 繪制圖片
于是嘗試了 Canvas 方式。首先,通過(guò)繪制一個(gè)棋盤格圖案來(lái)測(cè)試 Canvas 渲染效率:
// 簡(jiǎn)化的棋盤格Canvas繪制代碼示意:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const tileSize = 64;
let translateX = 0, translateY = 0, scale = 1;
// 繪制棋盤格
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(translateX, translateY);
ctx.scale(scale, scale);
const cols = Math.ceil(canvas.width / (tileSize * scale)) + 2;
const rows = Math.ceil(canvas.height / (tileSize * scale)) + 2;
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
ctx.fillStyle = (x + y) % 2 ? '#fff' : '#000';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
ctx.restore();
}
drawGrid();
Canvas 方式優(yōu)勢(shì)顯著:
- 靈活的滾動(dòng)和縮放功能
- 極大減少 DOM 節(jié)點(diǎn)
- 性能優(yōu)秀,支持高級(jí)動(dòng)畫和交互效果
經(jīng)過(guò)對(duì)比后,我最終選擇了 Canvas 方式,它提供了更大的靈活性和更好的渲染效率。
如何優(yōu)化圖片加載效率?
雖然 Canvas 的性能不錯(cuò),但加載數(shù)百萬(wàn)張小圖片仍然存在巨大挑戰(zhàn)。假設(shè)以一個(gè)標(biāo)準(zhǔn)的 1080p 屏幕為例:
- 寬度:1920px / 64px ≈ 30 張圖片
- 高度:1080px / 64px ≈ 17 張圖片
- 共需渲染 30 × 17 = 510 張圖片。
為了實(shí)現(xiàn)流暢滾動(dòng),頁(yè)面還需提前預(yù)加載周圍的圖片。如果將屏幕外 8 個(gè)方向的圖片也加載,意味著一次滾動(dòng)需要加載 4080 張圖片,這幾乎是不可能瞬間加載完畢的。
解決方案:合并小圖片到大圖塊。
將小圖片合并成大圖塊
為解決單獨(dú)圖片加載產(chǎn)生的網(wǎng)絡(luò)請(qǐng)求過(guò)多的問題,設(shè)計(jì)了一個(gè)后端 PHP 控制器,將 16 × 16(256張) 小圖片合并成一個(gè)大的圖片塊(每個(gè)塊1024×1024像素)。
用戶訪問頁(yè)面時(shí),瀏覽器將僅需加載較少數(shù)量的大圖塊,而非大量單獨(dú)圖片。這極大地減少了網(wǎng)絡(luò)請(qǐng)求次數(shù),提升加載速度。
例如上面的例子,現(xiàn)在只需加載 24 張 大圖塊,而非4080張單獨(dú)圖片:
- 寬度:5760px / 1024px ≈ 6 張
- 高度:3240px / 1024px ≈ 4 張
- 6 × 4 = 24 張圖片,負(fù)載完全可控!
未上傳圖片的位置顯示為“?”號(hào),清晰表示未填充。
一些提升用戶體驗(yàn)的小技巧
為了更好地隱藏大圖塊加載細(xì)節(jié),提升用戶體驗(yàn),作者采用了一些小技巧:
- 加載動(dòng)畫始終顯示為 64×64 的小塊,使用戶感知不到是加載了更大的圖片塊。
- 網(wǎng)格總是方形加載,避免出現(xiàn)邊界空白的視覺問題。
經(jīng)驗(yàn)與總結(jié)
回顧整個(gè)過(guò)程,從最初的逐個(gè)加載小圖片,到探索 Canvas,再到通過(guò)圖片塊合并優(yōu)化加載效率,每一步都是在不斷優(yōu)化用戶體驗(yàn)與性能之間的平衡。