Posted on October 21, 2023

前端性能优化总结

前端性能优化总结
性能优化的概念

这里我们所谈论的性能优化,其实就是如何要让页面更快地显示和响应,由于一个页面在它不同的阶段,所侧重的关注点是不一样的,所以如果我们要讨论页面优化,就要分析一个页面生存周期的不同阶段,其大致包含三个阶段:加载阶段、交互阶段和关闭阶段,关闭阶段不在性能优化范围内,这里不做讨论。

加载阶段

加载阶段的首要任务是将页面尽可能快的展示给用户,影响页面加载的相关资源,我们将之称为“关键资源”,其它的资源则为“非关键资源”。例如 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 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略,当垃圾回收操作发生时就会占用主线程,从而影响到其他任务的执行。此时,可以适当优化储存结构,尽量少地使用一次性对象,尽量重用这些对象,减轻垃圾回收的压力。

博客

关联内容