交互阶段的优化就是对框架的优化。
当与页面交互时,用户触发事件。有些事件会修改页面布局和样式。根据脚本的不同,浏览器可能需要多次经历渲染阶段才能不断地将新的位图或合成器帧传递到我们的屏幕。
FPS 和延迟
人眼每秒最多可以看到 1,000 帧 (FPS)。在慢速或静态场景中,我们很少能分辨出超过 30 FPS 的差异。然而,它在动态场景中变得很明显,例如玩第一人称射击游戏。如今,大多数显示器的刷新率为 60 FPS 或 60 赫兹。
理想情况下,浏览器需要在 1/60 秒内完成渲染阶段并交付一帧。即 16.67 毫秒。如果是这样,用户会看到流畅的动画并且不会感到滞后。
在交互阶段,通常是通过执行JavaScript来触发页面更新。大多数时候,它可能会触发reflow和repaint。
重排 reflow 和repaint repaint
当一行 JavaScript 更改元素高度时会发生什么? 高度修改不会影响 DOM 树。相反,它需要样式计算。
在样式计算结束时,渲染器进程将高度变化反映到布局阶段,从而导致元素几何信息的转换。因此,需要生成布局树。
布局树是其余阶段的依赖项,因此渲染器进程需要经历所有步骤。 这个过程称为重排 reflow。
许多属性检查在 JavaScript 中调用时可能会触发重排,例如“element.offsetLeft”。这是它们的完整列表。
reflow 最坏的情况是修改 DOM。一个例子是“document.body.appendChild(node)”。reflow 过程从第一阶段开始,即构建 DOM 树。
更改元素的背景颜色怎么样?
再次,让我们从样式计算开始。新的背景颜色不会修改元素的几何信息,因此渲染器进程会跳过它。它也不会创建新图层,因此让我们跳过图层阶段。在绘制阶段,渲染器进程需要生成新的绘制记录来反映背景颜色的更新。然后,它会经历其余的阶段。
这个过程称为repaint repaint。
reflow 和 repaint 都会降低渲染性能,原因有两个:
- reflow和repaint过程发生在主线程中,因此它无法处理用户交互触发的任何事件。当这种情况发生时,用户会感到滞后。
- 布局、图层和绘制阶段的计算过程非常昂贵。
由于 repaint 跳过了 Layout 和 Layer 阶段,因此它是比 reflow 相对更好的选择。
是否有任何更改根本不会触发 reflow 和 repaint?
是的。 CSS 动画就是一个很好的例子。
典型的 CSS 动画使用“transform”属性。修改“变换”值会跳过布局、图层和绘制阶段,并从合成器线程中的填充开始。
CSS动画在不占用主线程的情况下,不会阻塞用户的交互。这就是即使页面冻结,您仍然可以看到流畅的 CSS 动画的原因。
交互阶段的优化就是提高帧生成速度
JavaScript 执行、reflow 和 repaint 可能会减慢帧生成速度。 执行时,JavaScript 在主线程中运行。这里的想法是尽可能少地使用主线程。
减少执行 JavaScript 的时间长度
例如,一个重要的功能可能需要数百毫秒才能完成。它会阻塞主线程并降低性能。 我们可以将函数分成更小的函数,因此每个函数都不会花费很长时间。浏览器有助于优化运行功能时的任务。
Web Worker 是另一种选择。您可以将其作为渲染器进程中的独立线程进行操作。当脚本在 Web Worker 中运行时,主线程是空闲的。如果一段 JavaScript 不访问 DOM 和样式表,您可以将其移至 Web Worker。
避免执行 JavaScript 时的 reflow 和 repaint
当DOM树被修改时,渲染器进程会重新计算样式和布局。通常,计算在另一个任务中异步运行。
让我们看一个例子。
第一个任务完成 JavaScript 执行。然后另一个任务异步运行来计算样式和布局。
如果我们在脚本末尾检查元素高度怎么办?
当评估元素“offsetHeight”时,该值仍然是旧值,因为渲染器进程尚未计算样式和布局。渲染器进程同步开始计算,以便接收更新后的值。
在这种情况下,我们强制在 JavaScript 执行任务中进行样式和布局计算。计算会阻塞主线程,直到执行完成。
更糟糕的是什么呢?我们在 for 循环中评估属性。前面的过程不断发生,直到执行结束。大多数时候,页面上会出现明显的延迟。
在现实项目中,很难避免完全评估属性。不过,我们可以尽量减少使用。
使用 CSS 动画和“will-change”
CSS动画根本不使用主线程,所以我们可以尽可能地使用它。
同时,我们可以将“will-change”属性附加到动画元素上。具有“will-change”的元素会渲染在图层树中的独立图层上,从而进一步优化合成器线程中的帧生成。