
性能优化的概念
这里我们所谈论的性能优化,其实就是如何要让页面更快地显示和响应,由于一个页面在它不同的阶段,所侧重的关注点是不一样的,所以如果我们要讨论页面优化,就要分析一个页面生存周期的不同阶段,其大致包含三个阶段:加载阶段、交互阶段和关闭阶段,关闭阶段不在性能优化范围内,这里不做讨论。
加载阶段
加载阶段的首要任务是将页面尽可能快的展示给用户,影响页面加载的相关资源,我们将之称为“关键资源”,其它的资源则为“非关键资源”。例如 JavaScript 文件、Html 文件以及 Css 文件等都会影响到页面构建,它们明显应当是关键资源,而例如图片、视频等则为非关键资源。
那么关键资源的大小、个数以及请求速度就成为了影响首屏加载时间的最关键因素:
关键资源个数:
- 评估各资源的用途并评估是否可以直接移除
- 通过媒体查询避免首次渲染时加载不必要的 CSS 文件
- HTTP2.0 多路复用优化
关键资源大小:
- 压缩资源大小,gzip压缩、js 和 css压缩
- 减少资源体积,懒加载、代码分割以及 tree-shaking 等
关键资源的请求时间:
- cdn分担服务器压力并选择延迟更低的数据中心
交互阶段
页面加载完成显示给用户后,浏览器渲染进程的合成线程会同步显示器的更新频率,使得渲染进程生成一帧的时机与显示器的帧显示时机同步。假设在 60fps 的帧率下,每 16.6ms 为一帧,在此时间内,浏览器可能执行多次宏任务(包括它们的微任务队列),这也就是一轮事件循环,每次事件循环最多只会合成一帧画面(也有可能不合成),当事件交互、dom 构建以及帧合成完成后,浏览器进入空闲阶段,此时会执行一些低优先级,例如requestIdleCallback、垃圾回收任务等。
浏览器渲染进程的主线程同时承担了执行 JavaScript 脚本、构建 Dom 等任务,而如果 JavaScript 脚本执行占据了过长时间,就没有足够的时间进行 Dom 构建进而合成帧画面。因此,如何减少 JavaScript 脚本执行时间就成了重中之重,可以通过以下几种方式优化:
- 将一次执行的重函数分解为多个任务,使得每次的执行时间不要过久
- 采用 Web Workers,在 Web Workers 中执行 JavaScript 脚本
避免强制同步布局也是十分重要的一环。在 JS 脚本通过 DOM 接口执行添加元素或者删除元素等操作后,是需要重新计算样式和布局的,正常情况下,这些操作都是在另外的任务中异步完成的,这样做是为了避免当前的任务占用太长的主线程时间,所谓强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中。
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
//由于要获取到offsetHeight,
//但是此时的offsetHeight还是老的数据,
//所以需要立即执行布局操作
console.log(main_div.offsetHeight)
}
上面代码的执行逻辑中,将新的元素添加到 DOM 之后,我们又调用了main_div.offsetHeight来获取新 main_div 的高度信息。如果要获取到 main_div 的高度,就需要重新布局,所以这里在获取到 main_div 的高度之前,JavaScript 还需要强制让渲染引擎默认执行一次布局操作。我们把这个操作称为强制同步布局。
为了避免强制同步布局,我们可以调整策略,在修改 DOM 之前查询相关值。代码如下所示:
function foo() {
let main_div = document.getElementById("mian_div")
//为了避免强制同步布局,在修改DOM之前查询相关值
console.log(main_div.offsetHeight)
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
还有一种比强制同步布局更坏的情况,那就是布局抖动。所谓布局抖动,是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作,如下代码:
function foo() {
let time_li = document.getElementById("time_li")
for (let i = 0; i < 100; i++) {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById("mian_div").appendChild(new_node);
}
}
我们在一个 for 循环语句里面不断读取属性值,每次读取属性值之前都要进行计算样式和布局,这会大大影响当前函数的执行效率。这种情况的避免方式和强制同步布局一样,都是尽量不要在修改 DOM 结构时再去查询一些相关值。
减少回流重绘也能够起到优化页面性能的作用:
- 尽量在低层级的DOM操作
- css3 相关属性只会走合成线程,不影响主线程
- 操作 dom 时脱离文档流
- 将 DOM 的多个读操作(或者写操作)进行逻辑归纳,不要分离
在实现动画功能时,尽可能使用 CSS 合成动画实现。合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。所以要尽量利用好 CSS 合成动画,如果能让 CSS 处理动画,就尽量交给 CSS 来操作。
如果必须使用 JS 实现复杂动画功能,可以尝试使用 requestAnimationFrame 代替 setTimeout 和 setInterval 来更新视图,requestAnimationFrame 可以更高效和准确的执行帧合成任务,能够有效降低卡顿。
另外,JavaScript 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略,当垃圾回收操作发生时就会占用主线程,从而影响到其他任务的执行。此时,可以适当优化储存结构,尽量少地使用一次性对象,尽量重用这些对象,减轻垃圾回收的压力。