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

779 阅读6分钟

前言

页面优化,其实就是要让页面更快地显示和响应。由于一个页面在它不同的阶段,所侧重的关注点是不一样的,所以如果我们要讨论页面优化,就要分析一个页面生存周期的不同阶段。

通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。

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

这里我们需要重点关注 加载阶段交互阶段,因为影响到我们体验的因素主要都在这两个阶段,下面我们就来详细分析下。

加载阶段

我们先来分析如何系统优化加载阶段中的页面,还是先看一个典型的渲染流水线,如下图所示:

并非所有的资源都会阻塞页面的首次绘制,比如图片、音频、视频等文件就不会阻塞页面的首次渲染;而 JavaScript、首次请求的 HTML 资源文件、CSS 文件是会阻塞首次渲染的,因为在构建 DOM 的过程中需要 HTML 和 JavaScript 文件,在构造渲染树的过程中需要用到 CSS 文件。

我们把这些能阻塞网页首次渲染的资源称为 关键资源。基于关键资源,我们可以继续细化出来三个影响页面首次渲染的核心因素。

  1. 第一个是关键资源个数。关键资源个数越多,首次页面的加载时间就会越长。
  2. 第二个是关键资源大小。通常情况下,所有关键资源的内容越小,其整个资源的下载时间也就越短,那么阻塞渲染的时间也就越短。
  3. 第三个是请求关键资源需要多少个 RTT(Round Trip Time)。RTT 就是往返时延。它是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。通常 1 个 HTTP 的数据包在 14KB 左右,所以 1 个 0.1M 的页面就需要拆分成 8 个包来传输了,也就是说需要 8 个 RTT。

总的优化原则就是 减少关键资源个数降低关键资源大小降低关键资源的 RTT 次数

  • 如何减少关键资源的个数?一种方式是可以将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 async 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。
  • 如何减少关键资源的大小?可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。
  • 如何减少关键资源 RTT 的次数?可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。

交互阶段

接下来我们再来聊聊页面加载完成之后的交互阶段以及应该如何去优化。交互阶段的优化,其实就是在谈渲染进程渲染帧的速度,因为在交互阶段,帧的渲染速度决定了交互的流畅度。因此讨论页面优化实际上就是讨论渲染引擎是如何渲染帧的,否则就无法优化帧率。

我们先来看看交互阶段的渲染流水线(如下图)。和加载阶段的渲染流水线有一些不同的地方是,在交互阶段没有了加载关键资源和构建 DOM、CSSOM 流程,通常是由 JavaScript 触发交互动画的。

大部分情况下,生成一个新的帧都是由 JavaScript 通过修改 DOM 或者 CSSOM 来触发的。还有一部分帧是由 CSS 来触发的。

那接下来我们就可以讨论优化方案了。一个大的原则就是让单个帧的生成速度变快。所以,下面我们就来分析下在交互阶段渲染流水线中有哪些因素影响了帧的生成速度以及如何去优化。

1. 减少 JavaScript 脚本执行时间

有时 JavaScript 函数的一次执行时间可能有几百毫秒,这就严重霸占了主线程执行其他渲染任务的时间。针对这种情况我们可以采用以下两种策略:

  1. 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
  2. 另一种是采用 Web Workers。你可以把 Web Workers 当作主线程之外的一个线程,在 Web Workers 中是可以执行 JavaScript 脚本的,不过 Web Workers 中没有 DOM、CSSOM 环境,我们可以把一些和 DOM 操作无关且耗时的任务放到 Web Workers 中去执行。

2. 避免强制同步布局

所谓强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中。为了避免强制同步布局,我们可以调整策略,在修改 DOM 之前查询相关值。

3. 避免布局抖动

所谓布局抖动,是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。这种情况的避免方式和避免强制同步布局一样,都是尽量不要在修改 DOM 结构时再去查询一些相关值。

4. 合理利用 CSS 合成动画

合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。所以要利用好 CSS 合成动画,如果能让 CSS 处理动画,就尽量交给 CSS 来操作。

5. 避免频繁的垃圾回收

如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收。这样当垃圾回收发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会产生掉帧、不流畅的感觉。所以要尽可能优化储存结构,尽可能避免小颗粒对象的产生。