背景是在某项目中的其中一个页面,接入现场真实数据后,加载时间很长,长达10几秒,严重影响用户体验。
排查原因
在chrome
的DevTools
中,清缓存硬加载,可模拟用户第一次访问页面的场景。根据Network
中的若干指标,对性能瓶颈初步判断:
-
requests
:在HTTP / 1.0
或HTTP / 1.1
连接上,Chrome
每个主机最多允许六个同时TCP
连接,所以请求数过多会导致TTFB
等待时间过长。 -
xhr
:ajax
接口时间过长,瓶颈在于后端接口。 -
Finish
:Finish
时间远远大于DOMContentLoaded
和Load
时间,说明页面中的请求资源很大
优化方向
- 对于请求数过多的瓶颈,简单来说就是要减少请求数量。自上而下讨论,客户端(浏览器)页面要减少请求数量,将首屏不可见的资源放在首屏之后请求,将一些阻塞页面渲染的请求预加载或者懒加载;利用
HTTP 2
的多路复用特性,合并请求;服务端渲染。 -
ajax
接口时间过长,主要在后端接口的优化,Nodejs(BFF层)基于微服务的能力,所以要加快服务调用速度,减少服务调用数量,在业务上进行优化(缓存、批量接口、非必要数据异步请求...),当然自身代码层的运行效率也要考虑。 - 请求资源过大过多的问题,可优化img这类图片为懒加载(下文会提到),当大数据塞到数组对象里遍历渲染的时候,对不可见的组件进行懒加载,节省性能及页面渲染时间的开支;压缩图片格式,例如WebP格式;
对比 PNG 原图、PNG 无损压缩、PNG 转 WebP(无损)、PNG 转 WebP(有损)的压缩效果图
- 从用户体验角度出发,
loading
动画、图片的渐进式渲染(浏览器对一张图片的加载顺序基本上是下载了多少展示多少,让用户感觉很刻板生硬,渐进式渲染就是图片的内容从模糊到清晰的过程)、预加载(预见性的加载一些不可见区域的资源,提高用户在快速滚动浏览器时候的体验)。
懒加载的实践应用
上述优化方向中,作者优化了Nodejs
层的api
接口时间,缓存了部分业务逻辑,剥离了部分底层接口使其异步获取;同时选择懒加载优化方向,对前端组件及图片资源的加载进行优化。优化结果,肉眼可见。
懒加载并不是一个新鲜的名词,顾名思义,就是懒,现在不加载,稍后再加载,换个词说就是,按需加载。因为很多场景下,暂时看不见用不到的资源是不需要同时加载的,浪费时间开支同时,也消耗了不必要的CPU
、IO
等资源。例如图片懒加载在jQuery
时代就已经十分普及,在react
、vue
等前端MV*
框架的出现后,组件懒加载,SPA
单页应用中的路由懒加载,webpack
中对初始化不需要加载的代码块进行懒加载,从而优化性能...以下作者重点介绍下图片懒加载及vue
组件懒加载。
图片懒加载
对于一些视频图片web应用,图片懒加载几乎是必须要做的,可以大大提升用户体验。
原理解析
- 将需要懒加载的
img
标签的src
设置缩略图或者不设置src
,这里的占位图可以是缺省图,loading图; - 判断该img标签是否在浏览器可视区域,如果在可视区域,则将真实的图片url设置到img标签的src属性;
- 用户滚动浏览器,遍历需要懒加载的标签,根据步骤2判断并执行;
判断元素是否在浏览器可视区域
作者认为这是懒加载最重要的环节
getBoundingClientRect
MDN中的定义:Element.getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置。
// 获取元素的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)。
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,很稳。
引入
npm i vue-lazyload -S
// 在入口js中引入依赖,注册在vue实例上
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
lazyComponent: true
});
这里简单提一下该方案的组件懒加载方案,在源码L11-L16,利用render
来生成组件的内容this.$slots.default
,十分巧妙。
render (h) {
if (this.show === false) {
return h(this.tag)
}
return h(this.tag, null, this.$slots.default)
}
组件应用
// 原代码
<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打包,从代码块层面按需加载组件
- 懒加载依然"不够懒"
- 解决火焰图中看到的占据很长时间,阻塞页面渲染的请求
总结
优化无止境,常常是花了大力气,收效甚微。需要考虑时间和资源成本,优先解决投入产出比高的优化方向。以上是作者在实际项目中遇到的优化问题,仅供大家参考。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。