最近,看到一个名为 10MPage.com 的网站,目标是记录 2025 年互联网的时代印记。每个用户都可以上传一张 64x64 像素的小图片,形成一个庞大的互联网影像档案。

正如名字所暗示的,这个页面需要承载高达 1000 万张小图片。刚开始想到这个概念时,心想如何高效渲染这些图片?。在本文中,我将分享作者尝试的各种方案,以及最终实现的高效解决方案。

在你继续阅读之前,可以先访问一下 10MPage.com 看看能不能猜到我是如何实现的。如果你已经打开了10MPage,不妨也为自己上传一张图片,抢占一个位置吧!😉


🔍 HTML <img> 标签 vs Canvas

首先面临的选择是:用传统的 HTML 元素来渲染,还是用 Canvas 来进行绘制。

📌 方法一:大量单独的 <img> 标签

我最初使用了单独的 <img> 标签分别加载图片。我写了一个脚本,生成一个32x32(共1024张图片)的图片网格,用 Laravel Blade 模板进行渲染:

<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>

对应的 CSS 样式:

body {
    margin: 0;
    padding: 0;
    overflow: auto; /* 允许滚动 */
}

.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;
}

image.png

这种方式初步看起来不错,但潜藏几个严重问题:

  • 浏览器滚动性能差
  • DOM 节点数量庞大,性能开销大
  • 大量图片同时加载,网络请求数激增
  • 难以实现平滑滚动或高级动画效果

📌 方法二:Canvas 绘制图片

于是尝试了 Canvas 方式。首先,通过绘制一个棋盘格图案来测试 Canvas 渲染效率:

// 简化的棋盘格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 方式优势显著:

  • 灵活的滚动和缩放功能
  • 极大减少 DOM 节点
  • 性能优秀,支持高级动画和交互效果

经过对比后,我最终选择了 Canvas 方式,它提供了更大的灵活性和更好的渲染效率。


🚀 如何优化图片加载效率?

虽然 Canvas 的性能不错,但加载数百万张小图片仍然存在巨大挑战。假设以一个标准的 1080p 屏幕为例:

  • 宽度:1920px / 64px ≈ 30 张图片
  • 高度:1080px / 64px ≈ 17 张图片
  • 共需渲染 30 × 17 = 510 张图片。

为了实现流畅滚动,页面还需提前预加载周围的图片。如果将屏幕外 8 个方向的图片也加载,意味着一次滚动需要加载 4080 张图片,这几乎是不可能瞬间加载完毕的。

👉 解决方案:合并小图片到大图块


🖼️ 将小图片合并成大图块

为解决单独图片加载产生的网络请求过多的问题,设计了一个后端 PHP 控制器,将 16 × 16(256张) 小图片合并成一个大的图片块(每个块1024×1024像素)。

用户访问页面时,浏览器将仅需加载较少数量的大图块,而非大量单独图片。这极大地减少了网络请求次数,提升加载速度。

image.png

例如上面的例子,现在只需加载 24 张 大图块,而非4080张单独图片:

  • 宽度:5760px / 1024px ≈ 6 张
  • 高度:3240px / 1024px ≈ 4 张
  • 6 × 4 = 24 张图片,负载完全可控!

未上传图片的位置显示为“❓”号,清晰表示未填充。


🎩 一些提升用户体验的小技巧

为了更好地隐藏大图块加载细节,提升用户体验,作者采用了一些小技巧:

  • 加载动画始终显示为 64×64 的小块,使用户感知不到是加载了更大的图片块。
  • 网格总是方形加载,避免出现边界空白的视觉问题。

image.png


🌟 经验与总结

回顾整个过程,从最初的逐个加载小图片,到探索 Canvas,再到通过图片块合并优化加载效率,每一步都是在不断优化用户体验与性能之间的平衡。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.2k 声望105k 粉丝