跳到主要内容

图片懒加载

· 阅读需 4 分钟

Intersection Observer API

当页面上需要展示大量的图片时,只加载屏幕范围内我们看得见的图片

首先将 img 元素的 src 设置为默认占位的图片,设置自定义属性 data-src 为真实的图片地址

<div class="item">
<img
src="./default.png"
data-src="https://picsum.photos/400/600?r=1"
/>
</div>

监听这些元素与视口的相交情况

const ob = new IntersectionObserver(() => {
console.log('交叉改变后运行')
}, {
root: null, // 被观察对象的父级元素,默认是 null,表示视口
rootMargin: 0, // 扩张或收缩 root 的识别范围
threshold: 0,// 交叉的阈值 0-1 被观察对象交叉的程度
})

// 获取全部带有 data-src 属性的 img 元素
const imgs = document.querySelectorAll('img[data-src]')
imgs.forEach(() => {
ob.observe(img)
})

当元素出现在屏幕中时,替换 src 为 data-src 即可

const ob = new IntersectionObserver((entries) => {
// entries 是所有被观察元素的交叉情况
for(const entry of entries) {
if(entry.isIntersecting) { // entry.isIntersecting 是否相交
const img = entry.target // entry.target 被观察的元素
img.src = img.dataset.src // 替换 src
ob.unobserve(img) // 加载出来后就移除监听
}
}
}, {
threshold: 0,
})
// ...

其他方案

附上其他常见的图片懒加载方案

原生懒加载

利用 img 标签的原生属性 loading,添加懒加载功能

<img loading="lazy" src="image.jpg" alt="..." />

这个策略有一个动态的原则:

  1. Lazy loading加载数量与屏幕高度有关,高度越小加载数量越少,但并不是线性关系。4G 网络下的视口距离阈值是 1250 像素,3G 网络下是 2500 像素。
  2. Lazy loading加载数量与网速有关,网速越慢,加载数量越多,但并不是线性关系
  3. 滚动会触发图片懒加载,不会说滚动一屏后再去加载。
  4. 窗口resize尺寸变化也会触发图片懒加载。
  5. 根据滚动位置不同,Lazy loading会忽略头尾的图片请求。

预览图

我在相册应用中使用了 thumbnail 作为预览图,占用空间小,传输效率高

在图片上传时,后端使用 sharp 生成一张预览图,将预览图与原图一同保存在服务器中

import sharp from 'sharp'
// 使用 sharp 生成预览图
await sharp(buffer)
.resize(200, 200)
.jpeg({ quality: 80 }) // 设置图片质量
.toFile(fileThumbPath)

模糊图

同样的思路,也可以生成一张模糊图作为父元素的背景图

<div class="blur-load rounded-lg" style="background-image: url(./1-sm.png)">
<img loading="lazy" alt="9" class="w-full object-cover rounded-lg" src="..." />
</div>

初始情况下,img 标签设置为透明。当图片加载完成(img.complete 或 load 事件触发)后,img 标签恢复显示

这利用了一个特性:img 图片的层级要高于其本身或者父级的背景图片,当图片加载出来后自动覆盖背景图片

function loaded(div) {
div.classList.add('loaded');
}

const blurDivs = document.querySelectorAll('.blur-load');
blurDivs.forEach(div => {
const img = div.querySelector('img');

// 在加载完成的时候添加样式
if (img.complete) { loaded(div) }
else { img.addEventListener('load', () => loaded(div)) }
})
/* 加载完成之前保持透明 */
.blur-load > img {
opacity: 0;
transition: opacity 200ms ease-in-out;
}

.blur-load.loaded > img {
opacity: 1;
}