为什么要懒加载
图片是网页中最占带宽的资源。如果页面中有 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="文章封面">
- 第一层:JS 引擎接管
data-lazy-src属性,包裹骨架屏,IntersectionObserver 触发后加载 - 第二层:原生
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> 同时写了 src 和 data-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 模板注入配置,支持后台管理面板实时调节参数。
留言评论
期待你的想法评论加载中