软硬皆施

懒加载在前端性能优化的应用及原理

背景是在某项目中的其中一个页面,接入现场真实数据后,加载时间很长,长达10几秒,严重影响用户体验。

排查原因

chromeDevTools中,清缓存硬加载,可模拟用户第一次访问页面的场景。根据Network中的若干指标,对性能瓶颈初步判断:

  • requests:在HTTP / 1.0HTTP / 1.1连接上,Chrome每个主机最多允许六个同时TCP连接,所以请求数过多会导致TTFB等待时间过长。
  • xhrajax接口时间过长,瓶颈在于后端接口。
  • FinishFinish时间远远大于DOMContentLoadedLoad时间,说明页面中的请求资源很大

    优化方向

  1. 对于请求数过多的瓶颈,简单来说就是要减少请求数量。自上而下讨论,客户端(浏览器)页面要减少请求数量,将首屏不可见的资源放在首屏之后请求,将一些阻塞页面渲染的请求预加载或者懒加载;利用HTTP 2的多路复用特性,合并请求;服务端渲染。
  2. ajax接口时间过长,主要在后端接口的优化,Nodejs(BFF层)基于微服务的能力,所以要加快服务调用速度,减少服务调用数量,在业务上进行优化(缓存、批量接口、非必要数据异步请求…),当然自身代码层的运行效率也要考虑。
  3. 请求资源过大过多的问题,可优化img这类图片为懒加载(下文会提到),当大数据塞到数组对象里遍历渲染的时候,对不可见的组件进行懒加载,节省性能及页面渲染时间的开支;压缩图片格式,例如WebP格式

对比 PNG 原图、PNG 无损压缩、PNG 转 WebP(无损)、PNG 转 WebP(有损)的压缩效果图

  1. 从用户体验角度出发,loading动画、图片的渐进式渲染(浏览器对一张图片的加载顺序基本上是下载了多少展示多少,让用户感觉很刻板生硬,渐进式渲染就是图片的内容从模糊到清晰的过程)、预加载(预见性的加载一些不可见区域的资源,提高用户在快速滚动浏览器时候的体验)。

懒加载的实践应用

上述优化方向中,作者优化了Nodejs层的api接口时间,缓存了部分业务逻辑,剥离了部分底层接口使其异步获取;同时选择懒加载优化方向,对前端组件及图片资源的加载进行优化。优化结果,肉眼可见。

懒加载并不是一个新鲜的名词,顾名思义,就是懒,现在不加载,稍后再加载,换个词说就是,按需加载。因为很多场景下,暂时看不见用不到的资源是不需要同时加载的,浪费时间开支同时,也消耗了不必要的CPUIO等资源。例如图片懒加载在jQuery时代就已经十分普及,在reactvue等前端MV*框架的出现后,组件懒加载,SPA单页应用中的路由懒加载,webpack中对初始化不需要加载的代码块进行懒加载,从而优化性能…以下作者重点介绍下图片懒加载及vue组件懒加载。

图片懒加载

对于一些视频图片web应用,图片懒加载几乎是必须要做的,可以大大提升用户体验。

原理解析

  1. 将需要懒加载的img标签的src设置缩略图或者不设置src,这里的占位图可以是缺省图,loading图;
  2. 判断该img标签是否在浏览器可视区域,如果在可视区域,则将真实的图片url设置到img标签的src属性;
  3. 用户滚动浏览器,遍历需要懒加载的标签,根据步骤2判断并执行;

判断元素是否在浏览器可视区域

作者认为这是懒加载最重要的环节

getBoundingClientRect

MDN中的定义:Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。

1
2
3
4
5
6
7
// 获取元素的getBoundingClientRect属性
const rect = Element.getBoundingClientRect();

if(rect.top < document.documentElement.clientHeight) {
// 将top值与页面的clientHeight进行对比,若小于则为可视区域
...
}

PS:该方案需要监听scroll事件,注意节流处理。

Intersection Observer

MDN中的定义:IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。Intersection Observer API 允许你配置一个回调函数,每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0 // 目标(target)元素与根(root)元素之间的交叉度是交叉比(intersection ratio), 取值在0.0和1.0之间
}

var observer = new IntersectionObserver(() => {
// 回调函数,当目标元素和根元素交叉时触发
...
}, options);

var target = document.querySelector('#listItem');
// 添加目标元素,与根元素进行交叉状态比对
observer.observe(target);

PS:该方案较前者的优点就是不需要监听,其实兼容性在chrome中还不错。

vue 组件懒加载

这里的懒加载判断依据和图片类似,同样是要判断可视或者即将可视的时机,来控制组件的加载与否。当加载条件为false时,不做渲染,为true时则渲染,这里用v-if指令就可以实现。

在条件切换的同时,最好加入类似骨架屏的页面,来过渡用户体验。

项目实践

社区里这样的方案有很多,评估后决定采用 vue-lazyload,star 5.7k,recent updates is 2 months ago,很稳。

引入

1
2
3
4
5
6
7
8
npm i vue-lazyload -S

// 在入口js中引入依赖,注册在vue实例上
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload, {
lazyComponent: true
});

这里简单提一下该方案的组件懒加载方案,在源码L11-L16,利用render来生成组件的内容this.$slots.default,十分巧妙。

1
2
3
4
5
6
render (h) {
if (this.show === false) {
return h(this.tag)
}
return h(this.tag, null, this.$slots.default)
}

组件应用

1
2
3
4
5
6
7
8
9
10
// 原代码
<div class="camera-card-img" :style="{'backgroundImage': 'url(' + data._thumbnails + ')'}">

// 加入图片懒加载逻辑
<div class="camera-card-img" v-lazy:background-image="data._thumbnails">

<lazy-component>
// 需要懒加载的组件
...
</lazy-component>

优化结果

调用真实数据,控制变量,优化前:

52 requests, 19 img

加入图片懒加载:

38 requests, 12 img

当浏览器继续滚动的时候,图片依次加载,可以看到network中的request逐步增加至52,说明加载了剩余图片。

组件懒加载也是同样的效果,数据量小可能页面finish时间差感知不明显,可以加大模拟量至上千:

Finish时间10s,

Finish时间4s,速度提升显著。

PS:这里前后请求数不变的原因,是作者在模拟数据的时候重复了若干次真实数据,导致资源地址都是重复的,浏览器会缓存请求,所以导致请求数不变。

后续计划

其实可以看到,数据量大的时候,加载速度依旧很慢,还需要继续优化,可以从以下几个方向:

  • 缩略图格式(压缩资源大小)
  • 优化webpack打包,从代码块层面按需加载组件
  • 懒加载依然”不够懒”
  • 解决火焰图中看到的占据很长时间,阻塞页面渲染的请求

总结

优化无止境,常常是花了大力气,收效甚微。需要考虑时间和资源成本,优先解决投入产出比高的优化方向。以上是作者在实际项目中遇到的优化问题,仅供大家参考。

PalmerYe wechat
世界很大,圈子很小,欢迎关注,相互进步。
Fork me on GitHub