
页面的三个阶段
- 加载阶段
从发出请求到渲染出完整页面的过程,影响这个阶段的因素有网络和 JS 脚本。 - 交互阶段
页面加载完成到用户交互的整合过程,影响这个阶段的因素是 JS 脚本。 - 关闭阶段
用户发出关闭指令后页面所做的一些清理操作。
加载阶段
关键资源:能阻塞网页首次渲染的资源。
- 影响网页首次渲染的核心因素
- 关键资源个数。
- 关键资源大小。
- 请求关键资源需要多少个 RTT(Round Trip Time)。
RTT:从发送端发送数据开始,到发送端收到接收端的确认,总共经历的时延。通常一个 HTTP 数据包大小在 14KB 左右,一个 0.1M 的页面就需要拆成 8 个包来传输,即 8 个 RTT.
接收到 HTML 数据之后,预解析线程会快速扫描 HTML 中的关键资源,一旦扫描到,立马发起请求。
上图中 css、js 同时发起请求,故 RTT 按照文件大小最大的那个算。
- 如何减少关键资源个数?
- 内联 css、js
- js 加 async,await CSSlink 属性加标志等将其变为非关键资源
- 如何减少关键资源大小?
压缩资源,移除注释。 - 如何减少关键资源 RTT 次数?
- 减少关键资源个数和大小。
- 使用 CDN
交互阶段
交互阶段,帧的渲染速度决定了交互的流畅度。优化原则就是让单个帧的生成速度变快。
优化帧生成速度的手段:
- 减少 JS 脚本执行时间
JS 函数执行的时间可能有几百毫秒,这就影响了主线程执行其他渲染任务的时间。
有两种方法可以优化:
一种是将一次执行的任务分解为多个任务,使得每次执行的时间不要过久。
另一种是采用 Web Workers,将一些和 DOM 无关且耗时的任务放到 Web Workers 中去执行。 - 避免强制同步布局
强制同步布局:JS 强制将计算样式和布局操作提前到当前任务中。
- 正常布局的代码和 Performace 面板:
<div id="main_div"><li id="time_li">time</li><li>geekbang</li></div><p id="demo">强制布局</p><button onclick="foo()">Add new Element</button><script>function foo() {let main_div = document.getElementById("main_div");let new_node = document.createElement("li");let textNode = document.createTextNode("time.geekbang");new_node.appendChild(textNode);document.getElementById("main_div").appendChild(new_node);}</script>

从图中可以看出,执行 JS 添加元素和重新计算布局样式是在不同的任务中,这是正常的布局操作。
- 强制同步布局的代码和 Performance 面板
<div id="main_div"><li id="time_li">time</li><li>geekbang</li></div><p id="demo">强制布局</p><button onclick="foo()">Add new Element</button><script>function foo() {let main_div = document.getElementById('main_div')let new_node = document.createElement('li')let textNode = document.createTextNode('time.geekbang')new_node.appendChild(textNode)document.getElementById('main_div').appendChild(new_node)// 由于要获取到 offsetHeight,但此时 offsetHeight 还是老数据,所以要立即执行布局操作console.log(main_div.offsetHeight)}

从图中可以看出,执行 JS 添加元素和重新计算布局样式都是在当前脚本中执行触发。
为避免强制布局,可以在 DOM 修改之前查询相关值。
let main_div = document.getElementById("main_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("main_div").appendChild(new_node);
- 避免布局抖动
布局抖动:在一次 JS 执行过程中,多次执行强制布局和抖动操作。
- 布局抖动的代码和 Performance 面板
function foo() {let time_li = document.getElementById("time_li");for (let i = 0; i < 100; i++) {let main_div = document.getElementById("main_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("main_div").appendChild(new_node);}}

从上图可以看出,在 foo 函数内部重复执行样式计算与布局,大大影响当前函数执行效率。所以要尽量避免在修改 DOM 结构时再去查询一些相关值。
- 合理利用 CSS 合成动画
合成动画直接在合成线程上执行,不受主线程影响。
如果能提前知道对某个元素执行动画操作,最好将其标记为 will-change,告诉渲染引擎将该元素单独生成一个图层。
避免频繁的垃圾回收
如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。当垃圾回收操作发生时,就会占用主线程,从而影响其他任务执行。所以要尽可能的优化存储结构,避免小颗粒对象的产生。
