媒体资源懒加载
懒加载常识
页面中可能会出现很多媒体资源,比如图片列表,视频列表等等。加载首屏看不到的图片只会浪费带宽,而且影响首屏打开速度。
上述情况也可以通过分页控制,一次只加载少量资源,通过手动会滚动到底部进行翻页,批量加载,这是另一种处理方式,这种方式的确更合理,但是 SEO 就不够理想。
懒加载的方式,就是仅加载当前视口内的媒体资源,用户拉动滚动条,当需要展示的媒体资源进入页面后,再进行加载。
懒加载的设计方式就是监听相关媒体元素进入可见范围后,再对其资源进行加载。
滚动监听实现懒加载 兼容方案
通过滚动监听实现懒加载的方法很简单且兼容性高,但是代码成本高,且有一些缺陷。
首先懒加载的媒体资源不应该直接挂载,例如 <img />
元素的 src
属性,可以使用 data-lazy-src
来替换,此时图片不会加载,因为 src
属性为空,类似:
<img height="400" data-lazy-src="./1663728157316fdd4ca.jpeg" alt="" />
然后监听页面滚动,计算元素当前位置进入视口后,主动设置图片的 src
,类似:
window.onscroll = () => {
const img = document.querySelector("img[data-lazy-src]");
const rect = img.getBoundingClientRect();
// 元素进入视口的条件,实际上有多种情况,这里只是判断元素顶部进入视口
if (rect.top > 0 && rect.top < rect.height) {
img.setAttribute("src", img.getAttribute("data-lazy-src"));
}
};
此时图片不会一开始就加载,当滑动滚轮时,元素顶部进入视口后,图片才会加载。
来分析下这种方式可以进行的优化和其存在的缺陷。
优化:
- 元素进入视口的条件很多,一个角,一条边,或者全部进入,又或者全部覆盖,这些条件都需要优化,类似碰撞监听。
- 在计算时可以为视口大小进行偏移量填充,让元素距离视口一定偏移量就提前加载,提高加载体验。
- 必须让懒加载一开始就执行,不然首屏的懒加载元素因为没有滚动,会无法加载资源。
- 对回调进行节流,懒加载结束后关闭回调。
缺陷:
- 元素不一定可以通过视口的滚动条进行展示,元素可能存在于滚动容器中,此时需要监听滚动的事件源就不止
window
了,并且此时懒加载触发会不正常,例如懒加载元素在父级滚动元素的隐藏区域,也在视口中,对于视口的监听就会触发懒加载执行,但实际上元素还是不可见的。
IntersectionObserver 实现懒加载 首选方案
IntersectionObserver 兼容性欠佳,使用时注意兼容性。
IntersectionObserver 是新规范中,用于观察元素和指定元素展示范围内的交叉状态。默认情况下,IntersectionObserver 观察元素和视口的交叉状态。
通过设置交叉区域观察交叉状态,可以知道当前元素是否可见,那么懒加载就得以实现。
通过 IntersectionObserver 创建一个一个交叉区域观察者,以下代码引用自 MDN:
const intersectionObserver = new IntersectionObserver((entries) => {
// 如果 intersectionRatio 为 0,则目标在视野外,
// 我们不需要做任何事情。
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log("Loaded new items");
});
// 开始监听
intersectionObserver.observe(document.querySelector(".scrollerFooter"));
实例化 IntersectionObserver 时可以传递观察的回调,回调的第一个参数是一个 IntersectionObserverEntry 类型的数组,记录了被观察的所有目标和其观察区域的相关交叉状态,其中 IntersectionObserverEntry.intersectionRatio
记录了交叉区域与被交叉元素位置的比例,所以只要该值大于 0,就说明产生了交叉。
回到之前的 dom 结构:
<img height="400" data-lazy-src="./1663728157316fdd4ca.jpeg" alt="" />
那么懒加载代码实现可以类似:
const intersection = new IntersectionObserver((entries, /* 交叉区域观察者的实例 */ instance) => {
entries.map((item) => {
if (item.intersectionRatio > 0) {
const el = item.target;
el.setAttribute("src", el.getAttribute("data-lazy-src"));
// 加载资源后不再观察该元素(取消监听)
instance.unobserve(el);
}
});
});
document.querySelectorAll("img[data-lazy-src]").forEach((item) => {
// 使用交叉区域观察器观察相关懒加载元素(监听)
intersection.observe(item);
});
上面代码实例化了一个 IntersectionObserver,并选取 img[data-lazy-src]
元素进行观察,当发现元素进入视口后,加载图片并取消对元素的观察。懒加载就这样轻松的实现了。
IntersectionObserver 还有其他配置、方法和属性,可以设置缩放观察区域,也可以设置观察区域由哪个元素确定。参见 API。
IntersectionObserver 功能远不止实现懒加载,对于元素动画进入视口再次播放,高计算组件脱离可见区域停止计算等,都非常有用。
棘手的问题
无论使用哪种方式懒加载,都有一个问题就是图片在未加载时不知道其大小,未加载时总是 0 * 0,如果没有布局,可能会导致图片堆叠到一起,一次懒加载判断后会对大量堆叠在一起的图片进行加载,这就失去了懒加载的意义。解决方案只能通过布局或预先给图片设置大小,使其撑起页面。