前端页面性能优化之懒加载

简介

我们在进行页面性能优化的时候,如果只是局限于前端的页面性能优化,使用的方式以及方法是很有限的,那么通常我们能够想到的就是如何让我们的首屏加载更加流畅,提升用户体验。

懒加载就是其中的一个方面,合理的进行图片等各种资源的延迟加载,甚至是接口以及业务逻辑的延迟执行,做到给首屏最大的性能空间,达到页面首屏更快的渲染效果。

这种思维,适合很多前端工程化的场景。

实现思路

资源占位

在我们懒加载的时候,通常是需要跳过原本执行步骤。比如在我们懒加载图片的时候,需要进行标签的占位,要事先设定好图片的宽高,使用data-*属性设置好待请求的图片地址,这个时候图片还没有被浏览器下载。

1
<img class="lazy" data-original="https://xyz.abc.com/logo.png" />

由于没有设置src属性,所以data-original地址对应的图片不会进行加载。如果直接在src属性设置好图片地址,浏览器解析到则会直接下载,那么就失去了懒加载的意义。

监听scroll事件

在懒加载的时候,需要制定懒加载的容器。比如:某些可以滚动的内容列表区域。

但是在实际开发中,往往不会显式的指定容器,举个例子:手机端常常会有长列表,随整个页面滚动展现,这时如果进行懒加载优化,默认滚动容器应该为window对象。

判断资源位置是否在视窗内

这个功能是懒加载的核心,因为懒加载也称为是延迟加载,但是什么时候进行加载呢?一定是在用户看到这个或者即将看到这个资源的时候,提前进行加载。这样用户在看到这个资源的时候资源加载完毕给用户更好的体验。

借用图片进行说明:

懒加载视窗图

也就是绿色待加载的页面元素,即将进入蓝色可视区域或者已经进入可视区域之后执行懒加载方法。

判断加载资源或者执行相应的回调逻辑

这个功能可以是根据业务进行扩展或者更改。懒加载最初的构想只设计在了图片的标签上,因为大部分手机端的页面80%左右都是图片构成,能够合理的控制图片的加载就能够更好的改善用户体验。但是也有例外,随着前端功能的越来越强大,很多渲染工作都会交付给前端进行处理,很多业务逻辑也是前端进行控制。所以系统会希望扩展一些懒执行功能,保证首屏逻辑优先完成,后续渲染或者复杂操作延迟执行。

下面是部分懒加载的实现源码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function QLazyLoader(setting) {
let _setting = {
// 选择元素
selector: '.qLazy',
// 事件名
event: 'scroll',
// 默认绑定event的对象容器
container: setting.container || window,
// 获取src的data属性,默认data-attrbute
attribute: 'data-original',
// 设置懒加载类型为img 选项图片img 只执行回调call
loadtype: 'img',
// 回调事件,元素出现在视口中的function
appear: null,
// 触发load事件时执行的回调function
load: null
}
// 省略进行滚动监听
// 省略判断是否出现在视窗内
// 如果元素出现在视窗内
elements.each(function() {
let dom = this
let jqThis = $(dom)
let qSrc = jqThis.attr(setting.attribute)
let loadAction = function() {}
let loadedCall = function() {
elements = $(grepElements(elements))
if(setting.load){
setting.load.call(dom, jqThis, elements.length, setting)
}
}
if(/img|background-image/.test(setting.loadtype) && qSrc) {
let _img = jqThis
// 开始懒加载图片
if (!_img.attr('src')) {
jqThis.attr('src', qSrc)
}
_img.one('error', ()=>{
console.log('qSrc loaded error', qSrc)
})
}
if(/js/.test(setting.loadtype) && qSrc) {
loadAction = ()=>{
loadScript(qSrc, loadedCall)
}
}
if(/call/.test(setting.loadtype)) {
loadAction = loadedCall
}
jqThis.one(APPEAR_EVENT, (e)=>{
if(!dom.loaded) {
if(setting.appear) {
setting.appear.call(dom, $(this), elements.lenght, setting)
}
loadAction()
}
})
})
}

如何判断资源位置是否出现在视窗内

根据可视区域的高度和滚动高度以及元素距离页面顶端的距离进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function isElementInViewport(el){
// 获取div距离顶部偏移量
let offsetTop = el.offsetTop
// 获取屏幕高度
let clientHeight = document.documentElement.clientHeight
// 屏幕卷去的高度
let scrollTop = document.documentElement.scrollTop
if(clientHeight + scrollTop > offsetTop) {
console.log('进入可视区域')
}else{
console.log('没有进入可视区域')
}
}

<!-- 注意:document.documentElement的兼容性问题不在讨论范围之内 -->

getBoundingClientRect API

这个方法非常有用,经常用来确定元素相对于视口的位置。该方法会返回一个DOMRect对象,包含元素的left、top、width、height、bottom、right等六个属性。

其中left、right、top、bottom都是元素(不包括margin)相对于视口的原点(视口的上边界和左边界)的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用方法举例
let ro = object.getBoundingClientRect()
let Top = ro.top
let Bottom = ro.bottom
let Left = ro.left
let Right = ro.right
let Width = ro.width || Right - Left
let Height = ro.height || Bottom - top

function isElementInViewport(el){
let rect = el.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (document.documentElement.clientWidth || document.documentElement.clientHeight) &&
rect.right <= (document.documentElement.clientWidth || document.documentElement.clientHeight)
)
}

IntersectionObserver API

至于是否使用这个API,我觉得我们自己了解就好,因为浏览器实现不太乐观,处于w3c的草案阶段

IntersectionObserver

扩展与优化

添加节流

scroll事件,在滚动过程中,由于检测是否在视窗内频繁触发这个方法,因而频繁执行dom操作、资源加载等浏览器负担消耗比较严重的行为。如果不加以控制很可能导致UI停顿甚至浏览器奔溃。

1
2
3
4
// 添加节流控制
if(_setting.throttleMS > 0){
this.update = throttle(this.update, _setting.throttleMS)
}

添加仅仅垂直方向上的判断

通常懒加载情况scroll事件绑定在window对象,整个页面又是从上到下进行渲染,所以,横向的元素判断是否在视窗内通常没有意义。设置开关,减少横向判断逻辑,减少资源消耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 只判断垂直方向元素是否出现在视窗内
if (setting.vertical){
if(jqThis.height() <= 0 || jqThis.css('display') === 'none'){
return
}
if($.abovethetop(this, setting)) {
// nothing
}else if(!$.belowthefold(this, setting)){
jqThis.trigger(APPEAR_EVENT)
counter = 0
}else{
if(++counter > setting.failureLimit){
return false
}
}
}

扩展功能

前面说过,懒加载并不仅仅局限于图片的情况,有时候需要根据业务的需求可以扩展需要延迟加载的静态资源。比如:js外链脚本、css外链样式、视频、音频等。又或者只放出回调,后续具体的业务逻辑交由开发自行处理。具体思路,可以在初始化时,配置指定的参数决定当前懒加载实例执行哪种功能能加载。也可以在站位DOM中配置一些属性进行懒加载类型的判断,可以根据业务需要进行灵活的处理。

总结

懒加载对于前端的性能优化来讲,可以称得上是一种万金油,很多场景都可以使用这种方式来进行页面的体验方面的提升。但是懒加载的核心是判断元素是否在视窗内的操作,这种判断是要在视窗滚动过程中获取元素的宽高,导致页面频繁重绘,会耗费很大的性能开销。

而且在监听DOM方面,如果懒加载的逻辑是延迟渲染某段HTML的代码的话,那么在绑定DOM事件方面也是有更多详细的考虑才行,否则可能会出现监听事件失效的问题。

所以,在应用加载的场景,还是需要多多考虑,如果页面的资源比较少,整体逻辑比较简单,可以不使用懒加载,一次性加载完成体验可能还要优于懒加载的优化效果。

最后希望大家能够在工作中合理应用懒加载的思想,做到页面的更好的体验。

小伙子别走,如果帮到您你懂得☺
0%