浏览器渲染性能

155 阅读6分钟

如今,大多数性能建议都围绕页面加载时间 (PLT) 展开,这当然对成功至关重要。我们减少或删除请求,压缩我们的资产并缩小 JavaScript 和 CSS。这都是好东西,但这只是图片的一部分。用户在您的网站上花费的大部分时间不是等待它加载,而是使用它。因此,用户的挫败感可能来自糟糕的 UI 响应、缓慢的滚动和生涩的动画。从人们加载您的网站或应用程序的那一刻到他们离开的那一刻,他们都是您的责任。 这些帧中的每一帧的预算都刚刚超过 16 毫秒(1 秒 / 60 = 16.66 毫秒)。然而实际上,浏览器有家务工作要做,所以你所有的工作都需要在10ms内完成。当您未能满足此预算时,帧速率会下降,并且屏幕上的内容会抖动。这通常被称为jank,它会对用户体验产生负面影响。

1. JS/CSS > 样式 > 布局 > 绘画 > 合成#

完整的像素管道。

如果您更改“布局”属性,即更改元素几何形状的属性,例如其宽度、高度或左侧或顶部位置,浏览器将不得不检查所有其他元素并“重排”页面。任何受影响的区域都需要重新绘制,并且最终绘制的元素需要重新组合在一起。

2. JS/CSS > 样式 > 绘画 > 合成#

没有布局的像素管道。

如果您更改了“仅绘制”属性,例如背景图像、文本颜色或阴影,换言之,不影响页面布局的属性,则浏览器会跳过布局,但仍会绘制。

3. JS/CSS > 样式 > 复合#

没有布局或绘画的像素管道。

如果您更改了一个既不需要布局也不需要绘制的属性,并且浏览器跳转到只是进行合成。参考:CSS Triggers

  • 使用 transform and opacity 属性,不会触发页面绘制

  • 通过图层提升will-change和动画编排translateZ来减少绘画区域。 用will-change创建一个新的合成器层,浏览器兼容性问题,可以加上translateZ,但是,必须注意不要创建太多层,因为每一层都需要内存和管理。在“坚持仅合成器属性和管理层数

  • 使用 Chrome DevTools 绘制分析器来评估绘制复杂性和成本;尽可能减少 面板怎样看:web.dev/animations-…

优化的几个细节

1.布局和样式大量失效

这里真正重要的是您使多少文档树无效。在最坏的情况下,您可能会使整个文档树无效,这可能导致浏览器不得不重新计算每个元素的尺寸和位置。这是一个糟糕的主意,尤其是当浏览器忙于做动画或滚动(你好视差网站)之类的事情时。

div.classList.add('active'); // change the classes 
div.style.height = 200 + 'px' // change the styles directly
  1. 尽可能避免改变风格。如果一个元素不需要改变,就不要改变它。
  2. 如果您确实需要进行更改,请将它们应用到尽可能靠近目标元素的位置。

2.布局抖动(重)

for (var p = 0; p < paragraphs.length; p++) { 
    var para = paragraphs[p]; 
    var width = div.offsetWidth; 
    para.style.left = width + 'px';
}

在这个例子中,我遍历了一堆段落并设置每个段落的宽度以匹配某个目标 div。当您将此映射到浏览器必须执行的工作时,您会看到offsetWidth获取 div 的宽度,这需要布局(读取),然后立即设置第一段的宽度(写入)。写入使读取的布局计算无效,因为部分渲染树已更改。因此,当浏览器点击第二段时,它必须再次进行布局,而这又一次通过改变样式而失效 控制面板:会触发了“强制同步布局” 改正办法

var width = div.offsetWidth; 
for (var p = 0; p < paragraphs.length; p++) { 
    var para = paragraphs[p]; 
    para.style.left = width + 'px';
}

为了帮助保证读写顺序,Mozilla 的 Wilson Page 编写了一个名为 FastDOM 的小型库

3.减少合成过程

文章开头文案 devtools-waterfall.jpeg

3.Paint Storms

5.Expensive Paints

比如一张图片,绘制要48ms,避免在滚动或动画等运行时活动期间调整图像大小;它们非常昂贵

对于奖励积分,您还应该确保隐藏您的动画 GIF

6.动画中的垃圾收集

7. 昂贵的输入处理程序

我们即将结束,但在此之前,我们还有一个领域需要处理:输入。今天,我们有大量可以监听的输入方法和事件:滚动、触摸、鼠标和方向,仅举几例。

让我们看一下触摸事件。在大多数现代浏览器中,有一个称为合成器的单独线程来处理滚动和输入。它还处理图层创建并与 GPU 通信以在屏幕上移动这些图层。当您在触摸设备上滚动时,快速路径是合成器接收输入事件,并且可以简单地指示 GPU 移动图层,而无需绘画或任何其他工作。

如果您附加一个 touchstart 侦听器,那么合成器现在必须等待主线程在 JavaScript VM 中执行回调,然后才能继续。这是因为你可以打电话preventDefault(),所以应该防止触摸滚动。

但是可能主线程已经忙于做其他工作(样式计算、布局、其他 JavaScript),它可能不会立即执行 touchstart 回调。同时,合成器线程无法继续滚动页面:

image.png

这里的解决方案是让浏览器处理滚动和触摸,如果可以的话。如果你不能这样做,那么尽可能晚地绑定监听器。也就是说,如果您不需要将侦听器附加到元素,则不要在那里。其次,将侦听器绑定到尽可能靠近元素的位置,最好是在目标元素本身上。有时将全局侦听器附加到document.body然后将其用作委托是很诱人的,但问题是即使目标元素无处可见,您也会收到事件。换句话说,如果你有一个全局监听器的元素被隐藏或从 DOM 中分离出来,那么浏览器就会从合成器线程到主线程一无所获!

最后让我们谈谈去抖动事件。我们感兴趣的许多事件将在单个帧内多次触发。您可以做的最糟糕的事情之一是在这些回调中执行任何视觉工作,因为:

  1. 您将强制浏览器执行用户永远看不到的工作。例如,如果调度了三个滚动事件,那么前两个所做的可视化工作基本上是浪费时间。
  2. 事件的处理与浏览器的渲染管道不同步。

相反,您应该做的是简单地存储您感兴趣的值,然后安排 requestAnimationFrame 回调以在浏览器最方便的时间处理事情:

function onScroll(evt) { // Store the scroll value for laterz.
  lastScrollY = window.scrollY; // Prevent multiple rAF callbacks. 
  if (scheduledAnimationFrame) return;
  scheduledAnimationFrame = true; requestAnimationFrame(updatePage);
} 
window.addEventListener('scroll', onScroll, false); 

现在浏览器可以在单个帧内多次调用事件处理程序,但您只会对它执行一次,并且在渲染管道中的正确时间:requestAnimationFrame回调。

参考:calendar.perfplanet.com/2013/the-ru…