在 Web 上高效地加载和展示图片,尤其是当图片数量达到数千张时,是一个综合性的工程问题。本文对比了几种主流方案,并分享了在 6000+ 张高分辨率照片场景下最终落地的最佳实践。
方案一:原生 Lazy Loading
HTML 规范提供了最简单的懒加载方案:
<img src="photo.jpg" loading="lazy" />
优点:零 JavaScript、零配置、浏览器原生支持。
缺点:行为不可控,无法自定义加载时机和阈值。在图片列表特别长的场景下,浏览器可能一次性加载过多图片(部分浏览器默认阈值较大)。
方案二:IntersectionObserver
通过 IntersectionObserver API 实现手动的懒加载控制:
const observer = new IntersectionObserver((entries) => {{
entries.forEach(entry => {{
if (entry.isIntersecting) {{
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}}
}});
}}, {{ rootMargin: "200px" }});
document.querySelectorAll("img[data-src]")
.forEach(img => observer.observe(img));
优点:完全可控,可以自定义阈值、预加载距离、加载策略。
缺点:需要额外的 JavaScript,在极快滚动时可能有延迟。
方案三:虚拟列表 + 按需加载
对于超长列表,只保留可视区域的 DOM 节点并配合懒加载:
// 核心算法 const visibleRange = computeVisibleRange( scrollTop, viewportHeight, rowHeight ); renderItems(visibleRange.start, visibleRange.end);
优点:内存占用极小,适合数万级别的列表。
缺点:实现复杂度高,需要处理各种边缘情况。
混合策略:最佳实践
在实际项目中,我们发现单一方案无法覆盖所有需求。最终采用的混合策略如下:
| 层级 | 技术 | 作用 |
|---|---|---|
| DOM 管理 | 虚拟滚动 (Map 缓存) | 控制 DOM 节点数量在 150 以内 |
| 图片加载 | IntersectionObserver | 仅在可视 + 缓冲区时加载 |
| 缩略图质量 | Sharp 预生成 WebP | 体积小、质量高、加载快 |
| 回退方案 | 原生 loading="lazy" | JS 失败时的兜底 |
这个混合方案在实际项目中表现优异:首屏加载不到 0.2 秒,滚动流畅度达到 60fps,服务端负载降低 85% 以上。关键的经验是:不要迷信单一方案,不同场景需要不同的策略组合。
另外,一个容易被忽视的优化点是缩略图的色彩一致性。对于摄影类应用,缩略图和全尺寸图的色差会严重影响用户体验。我们的解决方案是使用相机内嵌的 JPEG 预览(Sony ARW 格式中提取),确保缩略图与全尺寸图色彩完全一致。