页面性能优化经验总结
页面性能优化
一、静态资源加载优化
1.1 提高静态资源加载速度
- 用字蛛对字体包进行压缩
- 对图片进行无损压缩
- 给静态资源上 CDN 加速 或 阿里云 OSS 对象存储
1.2 减少一次性请求加载的资源数量
- 图片懒加载
二、减少不必要的重新渲染
修改元素的样式后,再读取它的以下属性,会立刻强制触发页面重新计算 Layout
- offsetTop / offsetLeft / offsetWidth / offsetHeight
- scrollTop / scrollLeft / scrollWidth / scrollHeight
- clientTop / clientLeft / clientWidth / clientHeight
- getComputedStyle()
比如如下代码,通过不断访问 clientHeight 来修改 con 的高度
while (i < 20) {
i++;
let height = con.clientHeight + 1;
con.style.height = height + 'px';
}
这段代码执行的 Timeline 如下,最底下的紫色部分就是重新计算 Layout 布局来求 clientHeight 的耗时。整体耗时(js 代码执行 + 重新布局 + 重绘)约为 8 ms。
那现在我们换一个种写法,通过一个外部 height 变量来不断修改 con 的高度:
let height = 20;
while (i < 20) {
i++;
height++;
con.style.height = height + 'px';
}
可以看到,重新 Layout 的次数减少了,整体耗时也变为了 1ms 左右。
所以,在修改样式之后,尽量避免去访问这些需要重新布局才能计算出来属性,可以降低 layout 次数,也就降低了 js 执行耗时,进而提升页面性能。
三、动画优化
3.1 什么叫一个流畅的动画
一般我们的显示器是刷新率是 60 HZ,所以一个流畅的网页动画的要求就是 1 秒 60 帧,即一秒重新渲染页面 60 次,一次渲染出来的页面叫一帧,动画的本质就是帧的切换,每一帧间隔 16.67 毫秒。
页面重新渲染间隔大于 16.67 毫秒,动画就会产生卡顿;
页面重新渲染间隔低于 16.67 毫秒,就造成渲染次数冗余浪费浏览器性能。
3.2 动画执行的时机
简而言之,在一次事件循环 event loop 里,js 引擎线程处理异步队列的顺序如下:
- 执行一个宏任务
- 执行全部微任务
- 判断是否需要重新渲染,如果需要,则将执行权交给 GUI 渲染线程重新渲染页面
其中,window.requestAnimationFrame() 注册的回调,会在重新渲染(重绘)之前执行。
判断是否需要重新渲染,判断的条件有两个:
- 前面的同步代码、宏任务、微任务是否有对页面样式进行修改
- 上一次渲染时间到现在是否超过了 16.67 ms(这只是一个大约值,浏览器没有控制得那么精细)
所以,浏览器本身就用事件循环机制去将多个样式的改变合并到一次渲染里,将页面重新渲染频率维持在大约 16.67 毫秒及以上。
3.3 降低卡顿
3.3.1 卡顿的原因 → 渲染间隔大于 16.67 ms
我们分析一次渲染的流程:
- js 执行 → 修改样式
- 回流 reflow
- 重绘 repaint
这 3 步加起来的时间超过 16.67 ms,那么上一帧到下一帧的切换就不自然,用户视觉上就产生了卡顿。
降低卡顿,本质就是减少一次重新渲染的时间,所以现在我们需要分析这三步的耗时来源:
- js 引擎线程耗时 → js 计算任务过大,执行时间过长,阻塞了 GUI 渲染线程
- GUI 渲染线程回流、重绘耗时 → js 修改的样式过多,布局、重绘耗时久
3.3.2 降低 js 引擎线程耗时
- 使用算法进行计算优化
- 使用 web Worker 分离计算任务
3.3.3 降低 GUI 渲染线程耗时
- 减少一次事件循环里(同步代码+宏任务+微任务),对样式的修改量,减轻回流和重绘的压力
- 回流一定引起重绘,重绘不一定引起回流 → 尽量减少回流 → 尽量减少对元素几何样式的修改,比如宽、高
- 开启 GPU 硬件加速,这个在手机端效果尤其明显
3.3.4 window.requestAnimationFrame()
request Animation Frame: 请求动画帧
通过 window.requestAnimationFrame() 指定一个回调,这个回调可以在下次页面重新重绘之前执行。
它一般应用场景,是取代 setTimeout 、setinterval 定时器动画、CSS 动画。
那么,定时器动画有什么问题为什么需要被取代。先看代码:
// 定时器动画
var animation1 = ()=>{
setTimeout(()=>{
...修改样式
if(i++ < 100) animation1();
}, 16)
}
回想一下事件循环顺序:
- 执行一个宏任务
- 执行全部微任务
- 判断是否需要重新渲染,需要,则执行 requestAnimationFrame 任务,再重新渲染
定时器控制的是每 n 秒注册一个宏任务,而不是每 n 秒重新渲染一次。
在一次事件循环里,一个宏任务被执行后,js 修改了样式,浏览器也不一定会重新渲染,浏览器可能等到下一次事件循环再一起渲染,而中间没有渲染的那一次,就彻底没有了,这就叫 “丢帧”。
而且因为 js 线程修改两次样式 + GUI 渲染线程一次渲染两次样式修改
,无论对于 js 线程还是 GUI 渲染线程来说,耗时都加大了。这一帧耗时加多之后,后面的动画帧也会跟着受到影响,是一个累计效应。
总结:定时器动画不能很好地控制每一帧的展示,可能产生丢帧,耗时加大等情况。
所以,window.requestAnimationFrame() 应运而生。通过递归调用自身,我们能在每次页面重新渲染之前去修改一些样式,进而控制一个动画每一帧的展示。
// requestAnimationFrame 动画
var animation2 = ()=>{
requestAnimationFrame(()=>{
...修改样式
if(i++ < 100) animation2();
})
}
总结:
- 定时器注册的宏任务 → 宏任务执行,修改样式 ≠ 即将重新渲染
- requestAnimationFrame 注册的宏任务 → 宏任务执行,修改样式 = 即将重新渲染