在 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 格式中提取),确保缩略图与全尺寸图色彩完全一致。