图片懒加载
概念
是什么
图片懒加载,即图片延迟加载。图片未需要时,不加载图片资源;直到图片需要(或即将需要)时,加载图片资源。
为什么
减少不必要的资源请求。
- 视口区域(用户在网页上可以看到的区域)有限,用户一定时间内看到的内容是有限的,页面不一定要短时间内加载完所有资源内容。
- 网络的传输效率有限,服务器的处理能力有限,当资源较多时,完成所有资源请求需要较长的时间。
- 用户等待资源加载耐心有限,使用体验与白屏时间相关。
绑定监听器
实现
- 对所有图片都设置一个项目中的默认的图片,用作占位并表示未加载(也可不设置),将真正的图片路径存于
img
标签的自定义属性中。 - 使用一个数组,存储所有的未被加载的图片。
- 给所有图片的外部容器绑定一个滚动的监听事件,其中计算哪些未被加载的图片进入显示,将路径赋值给图片的
src
中。
html
<div class="pics">
<img class="lazy-load" src="../assets/0.png" data-src="src/assets/1.png"/>
</div>
js
const lazyImages = ref([])
const init = () => {
lazyImages.value = [].slice.call(document.querySelectorAll("img.lazy-load"))
const lazyLoad = () => {
const el = document.querySelector(".pics")
lazyImages.value.forEach((img) => {
if (img.offsetTop < el.clientHeight + el.scrollTop) {
img.src = img.getAttribute("data-src")
console.log(img.src)
img.classList.remove("lazy-load")
}
})
}
lazyLoad()
document.querySelector(".pics").addEventListener("scroll", lazyLoad)
}
优化
- 对滚动事件加上节流,避免触发过于频繁。
- 如果能提前得知所有图片的宽高,可以通过纯数学计算判断哪个元素被加载显示,不会引起过多的回流。(获取图片
offsetTop
值时,浏览器会暂停js执行,优先执行完回流重绘队列中的任务)
IntersectionObserver视口监听
IntersectionObserver
,一个异步观察目标元素与其祖先元素或顶级文档视口交叉状态的方法。相比于监听器,检测图片是否进入视口更为精确。
- 为需要懒加载的图片都绑定一个
IntersectionObserver
事件。 - 回调函数中,设置图片应有的路径,并停止对该图片的观察。
html
<div class="pics">
<img class="lazy-load" src="../assets/0.png" data-src="src/assets/1.png"/>
</div>
js
const lazyImages = ref([])
const init = () => {
lazyImages.value = [].slice.call(document.querySelectorAll("img.lazy-load"))
const lazyLoad = new IntersectionObserver((entries) => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.src
lazyLoad.unobserve(item.target)
}
})
})
lazyImages.value.forEach((image) => {
lazyLoad.observe(image)
})
}
与监听器比较
- 监听器中需要遍历未显示的图片,如果图片数量较多,js执行的耗时会较多。
- 而
IntersectionObserver
仅需对各个图片绑定一次监听即可。
优化
- 图片过多时,每个图片都绑定一个
IntersectionObserver
可能会增大性能开销。可以考虑图片分组监听。如10张图片一组,每组图片的第一张图片绑定IntersectionObserver
,触发时,请求该组图片的资源。(当然了,IntersectionObserver
自身采用了惰性计算,即仅在特定时机计算交叉状态、延迟处理等优化,性能较好) - 同上,可以考虑维护一个正在
IntersectionObserver
监听的图片队列,图片完成监听后退出队列。当队列数量少于一定阈值时,将后续未绑定IntersectionObserver
监听的图片加入队列并绑定监听。 - 总而言之,尝试避免过多监听器存在可能导致的性能问题。
组件库lazy
一般的组件库中,图片组件提供懒加载的配置,如TDesign
组件库的t-image
标签。
html
<t-image :lazy="true" class="lazy-load" src="src/assets/1.png"/>
与前两种方法比较
- 所需的时间与
IntersectionObserver
相近,优于监听器。 - 相比
IntersectionObserver
,只需设置属性即可,更为简捷。
实现原理
- 在
t-image
组件创建过程中,如果启用了lazy
,会为其外层包裹的div
绑定一个IntersectionObserver
。 - 在
t-image
组件取消挂载时,对存在的IntersectionObserver
进行关闭。
ini
setup(props: TdImageProps) {
const divRef = ref<HTMLElement>(null);
const imgRef = ref<HTMLImageElement>(null);
let io: IntersectionObserver = null;
const { src } = toRefs(props);
const renderTNodeJSX = useTNodeJSX();
onMounted(() => {
//在nuxt3中img的onload事件会失效
if (imgRef.value?.complete && !props.lazy) {
triggerHandleLoad();
}
if (!props.lazy || !divRef.value) return;
const ioObserver = observe(divRef.value, null, handleLoadImage, 0);
io = ioObserver;
});
onUnmounted(() => {
divRef.value && io && io.unobserve(divRef.value);
});
}
- 其中绑定
IntersectionObserver
的函数observe
,其核心代码如下。 - 与前面自己使用
IntersectionObserver
完成监听的操作相近。
ini
io = new window.IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
callback();
io.unobserve(element);
}
}
);
v-lazy
vue中有插件,可以实现图片懒加载。
html
<div class="pics">
<div v-for="item in pictures" :key="item">
<img v-lazy="item"/>
</div>
</div>
与前三种方法比较
- 性能与第二、三种相近,优于第一种。
- 引入组件后,开盒即用,如果所用组件库无相关配置,是个不错的选择。
实现原理
- 组件挂载时,为组件调用绑定
IntersectionObserver
的方法。
kotlin
public mount(el: HTMLElement, binding: string | DirectiveBinding<string | ValueFormatterObject>): void {
if (!el)
return
const { src, loading, error, lifecycle, delay } = this._valueFormatter(typeof binding === 'string' ? binding : binding.value)
this._lifecycle(LifecycleEnum.LOADING, lifecycle, el)
el.setAttribute('src', loading || DEFAULT_LOADING)
if (!hasIntersectionObserver) {
this.loadImages(el, src, error, lifecycle)
this._log(() => {
this._logger('Not support IntersectionObserver!')
})
}
this._initIntersectionObserver(el, src, error, lifecycle, delay)
}
IntersectionObserver
初始化的方法中,和前面的IntersectionObserver
一样,为组件绑定IntersectionObserver
。- 同时,根据是否需要延迟执行分两种处理方式。
typescript
private _initIntersectionObserver(el: HTMLElement, src: string, error?: string, lifecycle?: Lifecycle, delay?: number): void {
const observerOptions = this.options.observerOptions
this._images.set(el, new IntersectionObserver((entries) => {
Array.prototype.forEach.call(entries, (entry) => {
if (delay && delay > 0)
this._delayedIntersectionCallback(el, entry, delay, src, error, lifecycle)
else
this._intersectionCallback(el, entry, src, error, lifecycle)
})
}, observerOptions))
this._realObserver(el)?.observe(el)
}
- 对于无需延迟的组件,执行完关闭监听。
php
private _intersectionCallback(el: HTMLElement, entry: IntersectionObserverEntry, src: string, error?: string, lifecycle?: Lifecycle): void {
if (entry.isIntersecting) {
this._realObserver(el)?.unobserve(entry.target)
this._setImageSrc(el, src, error, lifecycle)
}
}
- 对于需延迟的组件,做异步处理。
typescript
private _delayedIntersectionCallback(el: HTMLElement, entry: IntersectionObserverEntry, delay: number, src: string, error?: string, lifecycle?: Lifecycle): void {
if (entry.isIntersecting) {
if (entry.target.hasAttribute(TIMEOUT_ID_DATA_ATTR))
return
const timeoutId = setTimeout(() => {
this._intersectionCallback(el, entry, src, error, lifecycle)
entry.target.removeAttribute(TIMEOUT_ID_DATA_ATTR)
}, delay)
entry.target.setAttribute(TIMEOUT_ID_DATA_ATTR, String(timeoutId))
}
else {
if (entry.target.hasAttribute(TIMEOUT_ID_DATA_ATTR)) {
clearTimeout(Number(entry.target.getAttribute(TIMEOUT_ID_DATA_ATTR)))
entry.target.removeAttribute(TIMEOUT_ID_DATA_ATTR)
}
}
}