沐光

记录在前端之路的点点滴滴

IntersectionObserver 与延迟加载

前言

随着项目图表使用的复杂程度的提升,“大数据”与多图表渲染带来的影响是页面响应慢、用户体验随时间越来越糟。通过 chrome 的 performance 分析出的原因是:接口耗时基本可忽略不计,页面渲染耗时才是对用户体验的真正影响。因此如何更“智能”的加载渲染的图表才是问题解决的关键,而这就要聊到“延迟加载”了。

延迟加载

延迟加载比较通俗的说法为“懒加载”,其使用的场景都是在页面加载时优先加载关键资源(比如整体的页面轮廓等),非关键资源则在需要时才进行加载。

那为什么需要延迟加载呢?有需必有求,首先看看直接加载所导致的一些问题:

  • 加载过多无用内容,造成数据流量的浪费,增加浏览器压力。
  • 渲染时间增长,用户等待时间增长,会造成部分用户流失。
  • 浪费处理时间、电池电量和其它系统资源

为了增强用户体验,因此我们需要进行延迟加载。

getBoundingClientRect

早期还没有 IntersectionObserver 时,为了支持延迟加载,我们常使用的方法是 Element.getBoundingClientRect()。通过该方法来获取目标元素的大小以及其相对于视口的位置,从而来判断是否加载需要渲染的内容。

其语法为:

1
2
3
4
5
6
7
8
9
const targetElement = document.getElementById('id');
const rectObject = targetElement.getBoundingClientRect();

rectObject.top // 元素上边到视窗上边的距离;
rectObject.right // 元素右边到视窗左边的距离;
rectObject.bottom // 元素下边到视窗上边的距离;
rectObject.left // 元素左边到视窗左边的距离;
rectObject.width // 是元素自身的宽
rectObject.height // 是元素自身的高

例子:

既然有了 getBoundingClientRect 方法,那么为什么还需要 IntersectionObserver 方法呢?这里引用 MDN 上的一段话:

过去,交集检测通常需要涉及到事件监听,以及对每个目标元素执行 Element.getBoundingClientRect() 方法以获取所需信息。可是这些代码都在主线程上运行,所以任何一点都可能造成性能问题。当网页遍布这些代码时就显得比较丑陋了。

简而言之,getBoundingClientRect 方法对于需要监听大量对象的情况,其处理并不是特别完美(比如无限滚动,scroll 事件发生过于密集,容易造成性能问题),而这种情况下 IntersectionObserver 可以比较完美的解决这个问题。

IntersectionObserver

首先 IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发(即我们不用监听 scroll 事件了)。此外,该观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

这里再一次引用 MDN 上的解释:

Intersection Observer API 会注册一个回调方法,每当期望被监视的元素进入或者退出另外一个元素的时候(或者浏览器的视口)该回调方法将会被执行,或者两个元素的交集部分大小发生变化的时候回调方法也会被执行。通过这种方式,网站将不需要为了监听两个元素的交集变化而在主线程里面做任何操作,并且浏览器可以帮助我们优化和管理两个元素的交集变化。

少了对滚动事件的处理,在不考虑兼容性的情况下,性能方面肯定优于 getBoundingClientRect。那么它具体该如何使用呢?Demo 如下:

1
2
3
4
5
6
7
8
9
10
// 创建一个 observer
const io = new IntersectionObserver(callback, options);
// 监听目标元素
io.observe(element);

// 停止目标元素的监听
io.unobserve(element);

// 终止对所有目标元素可见性变化的观察
io.disconnect();

上面代码中,callback 是触发可见行变化时的回调函数,options 是配置对象。

options

首先看看 options 配置,其总共有三个属性:rootrootMarginthreshold

root 指选定的目标容器,默认为浏览器的视窗。

rootMargin 定义元素的 marginviewport + rootMargin 为最终计算的视窗大小,这里引用一张图

threshold 属性决定了什么时候触发回调函数。它是既可以是一个单一的 number 也可以是一个 number 数组,其表示目标元素和 root 元素相交程度达到该值的时候,callback 回调函数将会被执行。

1
2
3
4
5
6
7
new IntersectionObserver(
(entries, observer) => { /* ... */ },
{
// 在相交程度为 0、25%、50%、75%、100% 时触发回调
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);

callback

当目标元素可见行变化时,其会触发 callback 函数,一般情况该函数会触发2次(进入和离开)。该函数有两个参数: IntersectionObserverEntry 对象观察者 的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const callback = function(entries, observers) {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.rootBounds
// entry.boundingClientRect
// entry.intersectionRect
// entry.intersectionRatio
// entry.isIntersecting
// entry.target
// entry.time
});
};

🔽[IntersectionObserverEntry]
time: 3893.92
🔽rootBounds: ClientRect
bottom: 920
height: 1024
left: 0
right: 1024
top: 0
width: 920
🔽boundingClientRect: ClientRect
// ...
🔽intersectionRect: ClientRect
// ...
intersectionRatio: 0.54
🔽target: div#observee
// ...

rootBounds: root 元素调用 getBoundingClientRect() 函数的返回值(默认是 viewport 视口)

boundingClientRect: 目标 observer 调用 getBoundingClientRect() 函数的返回值

intersectionRect: 上述两矩形的交集,并且有效的告诉你观测到的元素的哪一部分是可见的

intersectionRect: 该属性告诉你元素目前有多少是可见的,比率是多少

isIntersecting: 目标元素在 root 元素中可见性是否发生了变化(至少得达到 thresholds 数组中的一个阈值)

target: 被观测的目标 DOM 元素

time: 可见行发生变化的时间(单位: ms)

再次引用一张图片来解释上述部分参数:

请留意,你注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法。

实战:无限滚动

文章推荐

参考文章