页面性能:如何系统地优化页面? - 图1

页面的三个阶段

  • 加载阶段
    从发出请求到渲染出完整页面的过程,影响这个阶段的因素有网络和 JS 脚本。
  • 交互阶段
    页面加载完成到用户交互的整合过程,影响这个阶段的因素是 JS 脚本。
  • 关闭阶段
    用户发出关闭指令后页面所做的一些清理操作。

加载阶段

关键资源:能阻塞网页首次渲染的资源。

  • 影响网页首次渲染的核心因素
  1. 关键资源个数。
  2. 关键资源大小。
  3. 请求关键资源需要多少个 RTT(Round Trip Time)。
    RTT:从发送端发送数据开始,到发送端收到接收端的确认,总共经历的时延。通常一个 HTTP 数据包大小在 14KB 左右,一个 0.1M 的页面就需要拆成 8 个包来传输,即 8 个 RTT.

接收到 HTML 数据之后,预解析线程会快速扫描 HTML 中的关键资源,一旦扫描到,立马发起请求。

上图中 css、js 同时发起请求,故 RTT 按照文件大小最大的那个算。

  • 如何减少关键资源个数?
  1. 内联 css、js
  2. js 加 async,await CSSlink 属性加标志等将其变为非关键资源
  • 如何减少关键资源大小?
    压缩资源,移除注释。
  • 如何减少关键资源 RTT 次数?
  1. 减少关键资源个数和大小。
  2. 使用 CDN

交互阶段

交互阶段,帧的渲染速度决定了交互的流畅度。优化原则就是让单个帧的生成速度变快。

优化帧生成速度的手段:

  1. 减少 JS 脚本执行时间
    JS 函数执行的时间可能有几百毫秒,这就影响了主线程执行其他渲染任务的时间。
    有两种方法可以优化:
    一种是将一次执行的任务分解为多个任务,使得每次执行的时间不要过久。
    另一种是采用 Web Workers,将一些和 DOM 无关且耗时的任务放到 Web Workers 中去执行。
  2. 避免强制同步布局

    强制同步布局:JS 强制将计算样式和布局操作提前到当前任务中。

  • 正常布局的代码和 Performace 面板:
  1. <div id="main_div">
  2. <li id="time_li">time</li>
  3. <li>geekbang</li>
  4. </div>
  5. <p id="demo">强制布局</p>
  6. <button onclick="foo()">Add new Element</button>
  7. <script>
  8. function foo() {
  9. let main_div = document.getElementById("main_div");
  10. let new_node = document.createElement("li");
  11. let textNode = document.createTextNode("time.geekbang");
  12. new_node.appendChild(textNode);
  13. document.getElementById("main_div").appendChild(new_node);
  14. }
  15. </script>

正常布局 performance.png
从图中可以看出,执行 JS 添加元素和重新计算布局样式是在不同的任务中,这是正常的布局操作。

  • 强制同步布局的代码和 Performance 面板
  1. <div id="main_div">
  2. <li id="time_li">time</li>
  3. <li>geekbang</li>
  4. </div>
  5. <p id="demo">强制布局</p>
  6. <button onclick="foo()">Add new Element</button>
  7. <script>
  8. function foo() {
  9. let main_div = document.getElementById('main_div')
  10. let new_node = document.createElement('li')
  11. let textNode = document.createTextNode('time.geekbang')
  12. new_node.appendChild(textNode)
  13. document.getElementById('main_div').appendChild(new_node)
  14. // 由于要获取到 offsetHeight,但此时 offsetHeight 还是老数据,所以要立即执行布局操作
  15. console.log(main_div.offsetHeight)
  16. }

强制同步布局 performance.png
从图中可以看出,执行 JS 添加元素和重新计算布局样式都是在当前脚本中执行触发。

为避免强制布局,可以在 DOM 修改之前查询相关值。

  1. let main_div = document.getElementById("main_div");
  2. // 在修改 DOM 之前查询相关值,可避免强制同步布局
  3. console.log(main_div.offsetHeight);
  4. let new_node = document.createElement("li");
  5. let textNode = document.createTextNode("time.geekbang");
  6. new_node.appendChild(textNode);
  7. document.getElementById("main_div").appendChild(new_node);
  1. 避免布局抖动

    布局抖动:在一次 JS 执行过程中,多次执行强制布局和抖动操作。

  • 布局抖动的代码和 Performance 面板
  1. function foo() {
  2. let time_li = document.getElementById("time_li");
  3. for (let i = 0; i < 100; i++) {
  4. let main_div = document.getElementById("main_div");
  5. let new_node = document.createElement("li");
  6. let textNode = document.createTextNode("time.geekbang");
  7. new_node.appendChild(textNode);
  8. new_node.offsetHeight = time_li.offsetHeight;
  9. document.getElementById("main_div").appendChild(new_node);
  10. }
  11. }

布局抖动.png
从上图可以看出,在 foo 函数内部重复执行样式计算与布局,大大影响当前函数执行效率。所以要尽量避免在修改 DOM 结构时再去查询一些相关值。

  1. 合理利用 CSS 合成动画
    合成动画直接在合成线程上执行,不受主线程影响。

如果能提前知道对某个元素执行动画操作,最好将其标记为 will-change,告诉渲染引擎将该元素单独生成一个图层。

  1. 避免频繁的垃圾回收
    如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。当垃圾回收操作发生时,就会占用主线程,从而影响其他任务执行。

    所以要尽可能的优化存储结构,避免小颗粒对象的产生。