我在构建10MPage.com,目标是捕捉2025年的互联网的状态。每位用户都可以上传一个64x64像素的小图,为这个档案库贡献一份力量。
正如名字所暗示的,它需要能够处理一千万张小图片。当我第一次提出这个概念时,我担心如何高效渲染这些图片。在这篇文章里,我会讲讲我最初的尝试和最终的解决方案。
在你继续之前, ,先看一下10MPage.com ,看看你能否弄清楚它是怎么做到的。如果你已经到了10MPage,为什么不为自己申请一个呢?:)
图像标签 VS Canvas我首先需要做选择,是否使用HTML元素或全屏的画布。
分离图像标签我最初为每个方块分别用了单独的 <img
标签进行测试,生成了1024张图片,用以组成一个32x32的网格,然后使用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="第 {{$y}}行第 {{$x}}列的瓷砖"></div>
@endfor
</div>
@endfor
</div>
使用以下CSS:
body {
margin: 0;
padding: 0;
overflow: auto; /* 启用滚动条 */
}
.grid {
display: block;
position: relative;
width: 100%; /* 网格宽度为100% */
}
.row {
display: flex; /* 每一行都是一个弹性容器 */
}
.tile {
width: 64px;
height: 64px;
box-sizing: border-box;
border: 1px solid #ccc; /* 方块之间有视觉分隔线 */
}
.tile img {
width: 64px;
height: 64px;
}
看起来就是这样的:
这倒是没问题,但是有几个方面可能会有点麻烦。
- 浏览器滚动条
- 大型DOM结构
- 按需加载
- 许多小图片
接下来的做法是直接在画布上进行,为了简单起见,我决定只是画一个棋盘。添加滚动也很简单,它看起来像这样。
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const tileSize = 64;
let scale = 1;
let translateX = 0;
let translateY = 0;
let isPanning = false, startX, startY;
// 绘制棋盘网格
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; // 额外的格子用于溢出
// 根据当前平移和缩放计算起始列和行索引
const startCol = Math.floor(-translateX / (tileSize * scale));
const startRow = Math.floor(-translateY / (tileSize * scale));
for (let x = startCol; x < startCol + cols; x++) {
for (let y = startRow; y < startRow + rows; y++) {
// 交替填充黑白格子
ctx.fillStyle = (x + y) % 2 === 0 ? 'black' : 'white';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
ctx.restore();
}
// 以鼠标位置为中心的平滑缩放
function zoom(event) {
event.preventDefault();
const zoomIntensity = 0.05; // 更小的值可以获得更平滑的缩放效果
const mouseX = event.clientX;
const mouseY = event.clientY;
const wheelDelta = event.deltaY > 0 ? -1 : 1; // 滚动方向
// 计算缩放因子(更小的值可以获得更平滑的过渡效果)
const zoomFactor = Math.exp(wheelDelta * zoomIntensity);
// 调整平移以使缩放朝向鼠标位置
translateX -= (mouseX - translateX) * (zoomFactor - 1);
translateY -= (mouseY - translateY) * (zoomFactor - 1);
// 应用缩放因子
scale *= zoomFactor;
drawGrid();
}
// 开始平移
function startPan(event) {
isPanning = true;
startX = event.clientX - translateX;
startY = event.clientY - translateY;
}
// 平移
function pan(event) {
if (!isPanning) return;
translateX = event.clientX - startX;
translateY = event.clientY - startY;
drawGrid();
}
// 结束平移
function endPan() {
isPanning = false;
}
// 事件监听器
window.addEventListener('wheel', zoom); // 窗口添加滚动事件监听器
canvas.addEventListener('mousedown', startPan);
canvas.addEventListener('mousemove', pan);
canvas.addEventListener('mouseup', endPan);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
drawGrid(); // 调整屏幕大小时重绘网格
});
// 初始绘制网格
drawGrid(); // 调整屏幕大小时重绘网格
</script>
</body>
这种方法挺不错,因为它允许我通过代码来展示一切,这会让更复杂的特性更容易实现。
选择使用图像标签还是画布(canvas)最后我选择了 canvas,因为它的灵活性比 div 更强,能够实现加载动画、平滑滚动、懒加载这些特性,并且完全通过代码渲染,提供更大的控制范围。
但加载很多小图片会带来很多负担,因此我想把这些小图片打包成更大的组来减少这种负担。
房瓦送货优化单独加载每张图片会增加很多网络请求。我们快速计算一下1080p屏幕的情况。宽度是1920像素,等于1920 / 64 = 30个图块。高度是1080像素,等于1080 / 64 ≈ 17个图块。所以在全高清显示器上,渲染一整屏瓦片需要渲染30 * 17 = 510个小图片。
但我们必须能够滚动!并且在滚动时,我不希望在渲染之前显示很多加载图标。这意味着我们需要预先加载周围的图片。如果我们想要预加载周围的图片,我们就需要加载周边八个图块。想象这个黑色矩形是我们正在查看的显示区域。
*那就会变成 510 8 = 4080 张图!**
这么快渲染这么多图像是不现实的。解决办法是把各个小图块组合成更大的块状。
使用 PHP,我编写了一个控制器,生成一个包含 1616 个方块的图片。每个块(每块)的宽度和高度都是 64 16 = 1024 像素。你可以访问 10MPage 并查看浏览器的网络标签来看到这一点。
脚本会在未填的空白处加上问号。
所以,从原本需要4080张图片(1920 3 = 5760像素,1080 3 = 3240像素),现在只需要24张图片,计算是这样的:5760像素除以1024约等于6(四舍五入到最接近的整数),3240像素除以1024约等于4(四舍五入到最接近的整数),6乘以4等于24。这完全做得到!
隐藏积木我已经做了一些事情来隐藏,让瓷砖看起来是以更大的块来加载的,而不是真正的块状加载。
加载画面通常有 64x64 的图块 网格布局总是被渲染成正方形为了隐藏网格底部或右侧的大空白区域,网格将永远不会加载非正方形的块,而是始终加载正方形的块。你不会看到底部有一个块,而其左边或右边是空白的。
感谢你阅读这篇文章,希望你有所收获。
如果你有所收获,为什么不把你最喜欢的编程语言、加密货币或者宠物添加到10MPage呢?而且这是完全免费的!
原发布于https://dev.to/vincentbean/how-i-managed-to-render-10-million-small-images-on-a-webpage-1f1l 在2025年1月12日。