网站图片懒加载优化实践

AI 智能总结

为什么要懒加载

图片是网页中最占带宽的资源。如果页面中有 20 张图片,且每张 200KB,首屏就需要下载 4MB 数据。而用户可能只看了前几张就离开了——这意味着大量的带宽和加载时间被浪费了。

懒加载的核心思路是:只加载用户视口内和即将进入视口的图片,其余图片等用户滚动到附近再加载。

Lumin 主题的自研方案

Lumin Blog 没有使用任何第三方懒加载库,而是基于 IntersectionObserver 自研了一套完整的图片懒加载引擎(lazy-load.js),支持以下特性:

  • 骨架屏动画:默认的 shimmer 光波效果,视觉反馈良好
  • 自定义占位图:可配置一张模糊占位图作为背景
  • 自上而下顺序加载:图片按 DOM 位置排序,瀑布流式逐个浮现
  • Banner 智能排除:首屏大图直接 eager 加载,不经过懒加载管线
  • 加载失败回退onerror 时显示 actualSrc 或原始 URL,避免永久空白

架构:双层保障

1<!-- 模板只写 data-lazy-src,不写 src,避免浏览器+JS 双重下载 -->
2<img data-lazy-src="/images/cover.webp"
3     loading="lazy"
4     alt="文章封面">
  1. 第一层:JS 引擎接管 data-lazy-src 属性,包裹骨架屏,IntersectionObserver 触发后加载
  2. 第二层:原生 loading="lazy" 作为兜底,即使 JS 未执行,浏览器也能懒加载

处理流程

 1模板渲染  <img data-lazy-src="...">
 2                
 3     lazy-load.js processImage()
 4     ├─ 已缓存?→ 跳过
 5     ├─ 未缓存  src = about:blank(防止浏览器过早下载)
 6     ├─ 包裹 .lazy-load-wrapper 骨架屏
 7     └─ IntersectionObserver.observe(img)
 8                
 9     图片进入视口(提前 rootMargin 像素触发)
10                
11     加入 loadQueue   DOM 位置排序
12                
13     processQueue() 依次加载(并发 2,间隔 80ms
14                
15     new Image() 下载完成  img.src 赋值
16                
17     移除 data-lazy-src  CSS 淡入动画
18                
19     300ms 后移除骨架屏 wrapper

骨架屏设计

加载中提供两种视觉模式,在 hugo.toml 中配置:

1[params.lazyLoad]
2  enable = true
3  concurrentLoads = 2                          # 并发数(≤4)
4  rootMargin = "200px"                         # 提前加载距离
5  placeholder = ""                             # 自定义占位图URL(留空=骨架屏动画)

模式一:骨架屏动画(默认)

未配置 placeholder 时,显示灰色背景 + 白色 shimmer 光波动画 + 中央图片图标脉冲:

 1.lazy-load-wrapper::before {
 2  background: linear-gradient(90deg,
 3    transparent 0%,
 4    rgba(255,255,255,0.3) 20%,
 5    rgba(255,255,255,0.5) 40%,
 6    rgba(255,255,255,0.3) 60%,
 7    transparent 100%
 8  );
 9  animation: lazyShimmer 1.5s ease-in-out infinite;
10}

模式二:自定义占位图

配置了 placeholder URL 后,骨架屏背景替换为占位图(如模糊缩略图),加载完成后平滑过渡到原图。

加载顺序:自上而下瀑布流

一个常见问题是:当首页有 33 篇文章卡片时,如果同时加载所有可见封面,会造成网络拥塞和页面卡顿。

v2 引擎的解决方案:

 1// 1. 队列按 DOM 位置排序
 2function sortQueueByDOM() {
 3  loadQueue.sort((a, b) => {
 4    var pos = a.compareDocumentPosition(b);
 5    if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
 6    if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
 7    return a.getBoundingClientRect().top - b.getBoundingClientRect().top;
 8  });
 9}
10
11// 2. 严格控制并发 + 错开时机
12var batch = loadQueue.splice(0, CONCURRENT_LOADS); // 每次 2 张
13batch.forEach((img, idx) => {
14  setTimeout(() => loadImage(img, callback), idx * 80); // 间隔 80ms
15});

效果:第一排的两张封面先加载,80ms 后第二排开始,以此类推——产生自然的自上而下瀑布流。

踩过的坑

坑 1:双重下载导致加载卡死

现象:文章卡片封面一直显示骨架屏,图片永远不出现。

原因:模板中 <img> 同时写了 srcdata-lazy-src,指向同一 URL。浏览器通过 src 开始下载,但 CSS img[data-lazy-src] { opacity: 0 } 将其隐藏;同时 lazy-load.js 又创建 new Image() 重复下载。两次下载产生竞态,一旦 JS 侧下载超时或失败,图片就卡住。

修复:模板只保留 data-lazy-src,JS 始终将 src 置为 about:blank,彻底消除双重下载。

坑 2:Banner 轮播图被误拦截

现象:首页 Banner 五张图片轮播一圈后,shimmer 动画重新出现。

原因lazy-load.js 的选择器包含了 .banner-slide-image。Banner 图片本已是首屏 hero image,被 JS 拦截后 src 被替换,加载时序被打乱。

修复:从 lazy-load 选择器中彻底移除 Banner 选择器,processImage() 增加 img.closest('.banner-slide') 排除检查;Banner 图片显式设置 loading="eager" fetchpriority="high"

坑 3:并发过高导致页面卡顿

现象:首页打开时 20+ 张封面同时请求,浏览器瞬间发出大量网络连接。

原因concurrentLoads 设置为 6,且 rootMargin: 200px 导致大量图片同时进入预加载区。

修复:并发降至 2,图片间 80ms 错开,批次间额外 50ms 停顿;队列按 DOM 位置排序确保视觉效果。

进阶:响应式图片懒加载

结合 <picture> 元素和 srcset,可以实现真正的响应式懒加载:

1<picture>
2  <source data-srcset="/images/photo-800.webp" media="(min-width: 800px)">
3  <source data-srcset="/images/photo-400.webp" media="(min-width: 400px)">
4  <img data-src="/images/photo-200.webp"
5       loading="lazy"
6       alt="响应式图片"
7       class="lazy-img">
8</picture>

移动端加载小图,桌面端加载大图,进一步节省带宽。

总结

策略适用场景
原生 loading="lazy"简单页面、不关心视觉过渡效果
自研骨架屏引擎博客/CMS,需要统一的加载视觉和精细控制
两者结合(Lumin 方案)最佳实践:JS 引擎处理封面/相册/卡片,原生属性兜底

Lumin 主题的懒加载引擎覆盖了文章封面、相关推荐、相册、画廊、侧边栏等全站图片,前端 window.siteConfig.lazyLoad 通过 Hugo 模板注入配置,支持后台管理面板实时调节参数。

留言评论

期待你的想法

评论加载中