浏览器Frame详解(译文)

1,518 阅读6分钟

前言

其他开发人员经常问我像素工作流程的各个部分,以及什么时候触发以及为什么触发,所以我认为可能值得为将像素运送到屏幕提供一些参考

正文

一张图解释渲染流程 The full process of getting pixels to screen.

包含的进程

  • Renderer Process(渲染器进程):它包含多个线程,这些线程一起负责将页面显示在屏幕上的各个方面。这些线程是合成器、平铺工作线程(Tile Worker)和主线程。
  • GPU Process(GPU进程) 当提交帧时,GPU进程将上传任何瓦片和其他数据(如四顶点和矩阵)到GPU,以实际将像素推到屏幕上。GPU进程包含一个单独的线程,称为实际执行工作的GPU线程。

渲染器进程的线程

  • Compositor Thread(合成器线程) 这是第一个被告知vsync事件的线程(这是操作系统告诉浏览器制作新帧的方式)。它还将接收任何输入事件。如果可以的话,合成器线程将避免转到主线程,并尝试将输入(比如滚动投掷)转换为屏幕上的移动。它将通过更新层位置并通过GPU线程将帧直接提交到GPU来实现这一点。如果由于输入事件处理程序或其他视觉工作的原因无法做到这一点,那么将需要主线程。
  • Main Thread(主线程) 这就是浏览器执行我们都知道地方:JavaScript、样式、布局和绘制

image.png

  • Compositor Tile Worker(s) 由合成器线程派生的一个或多个工作线程,用于处理光栅化任务。我们稍后再多谈一谈。

在许多方面,您应该将Compositor Thread视为“大老板”。虽然它不运行JavaScript、Layout、Paint或任何其他功能,但线程完全负责启动主线程工作,然后将帧发送到屏幕。如果它不必等待输入事件处理程序,那么它可以在等待主线程完成工作的同时发送帧。

事务的流动

The main thread in all its glory.

让我们逐步了解从vsync到像素的流程,并讨论在“完整”版本的事件中事情是如何进行的。值得记住的是,浏览器不需要执行所有这些步骤,这取决于所需的内容。例如,如果没有新的HTML要解析,那么ParseHTML就不会启动。事实上,通常情况下,提高性能的最佳方法只是消除对部分流进行执行的需要!

同样值得注意的是,样式和布局下方的红色箭头似乎指向requestAnimationFrame。完全有可能在代码中意外触发这两种情况。这被称为强制同步布局(或样式,视情况而定),通常对性能不利。

  • 帧开始(Vsync)。Vsync被触发,一帧开始。
  • 输入事件处理程序(Input event handlers)。输入数据从合成器线程传递到主线程上的任何输入事件处理程序。所有输入事件处理程序(触摸移动、滚动、点击)都应该首先启动,每帧启动一次 详见这个jsfiddle,大家可以试试,你可以发现mousemove回调和requestAnimationFrame回调的调用频率是完全一致的,但情况并非如此;调度器进行尽最大努力的尝试,其成功与否因操作系统而异。在用户交互和事件到达要处理的主线程之间也存在一些延迟。
  • requestAnimationFrame。这是对屏幕进行可视化更新的理想场所,因为您有新的输入数据,而且它尽可能接近vsync。其他视觉任务,如样式计算,将在该任务之后进行,因此它是变异元素的理想位置。如果你变异——比如说——100个类,这不会导致100种风格的计算;它们将被分批处理。唯一需要注意的是,您不会查询任何计算样式或布局属性(如el.style.backgroundImage或el.style.offsetWidth)。如果你这样做,你会将recalc样式、布局或两者都向前推进,导致强制同步布局,或者更糟的是,导致布局混乱。(看不懂,不知道在说什么)
  • 分析HTML(Parse HTML) 处理任何新添加的HTML,并创建DOM元素。在页面加载期间或appendChild等操作之后,您可能会看到更多这样的情况。
  • 重新计算样式(Recalc Styles) 样式是为任何新添加或变异的内容计算的。这可能是整棵树,也可能是下一个范围,具体取决于更改的内容。例如,更改主体上的类可能影响深远,但值得注意的是,浏览器在自动限制样式计算的范围方面已经非常聪明了。
  • 布局(Layout) 计算每个可见元素的几何信息(每个元素的位置和大小)。这通常是对整个文档进行的,通常使计算成本与DOM大小成比例。
  • 更新图层树(Update Layer Tree) 创建堆叠上下文和深度排序元素的过程。
  • 绘制(Paint) 这是一个由两部分组成的过程中的第一部分:绘制是对任何新的或在视觉上发生变化的元素进行绘制调用的记录(在这里填充一个矩形,在那里写文本)。第二部分是光栅化(见下文),在这里执行绘制调用,并填充纹理。这部分是绘制调用的记录,通常比光栅化快得多,但这两个部分通常统称为“绘制”。
  • 复合(Composite) 计算层和平铺信息,并将其传递回复合线程以供其处理。这将考虑到诸如将要更改、重叠元素和任何硬件加速画布等因素。
  • 光栅排定和光栅化(Raster Scheduled and Rasterize):现在执行“绘制”任务中记录的绘制调用。这是在Compositor Tile Workers中完成的,其数量取决于平台和设备功能。例如,在安卓系统上,你通常可以找到一个工作人员,在桌面上,你有时可以找到四个。光栅化是根据层来完成的,每个层都由瓦片组成。光栅化从早期的 Full-screen Rasterization基本都进化到了现在的Tile-Based Rasterization, 也就是不是对整个图像做光栅化,而是把图像分块(tile,亦有翻译为瓦片、贴片、瓷片…)后,再对每个tile单独光栅化。光栅化好了将像素填充进纹理,再将纹理上传至GPU。
  • 帧结束(Frame End):随着各个层的瓦片全部光栅化,任何新的瓦片以及输入数据(可能已在事件处理程序中更改)都会提交到GPU线程。
  • 帧传送(Frame Ships) 最后,但并非最不重要的是,瓦片由GPU线程上传到GPU。GPU使用四边形和矩阵(所有通常的GL优点)将瓷砖绘制到屏幕上。
  • requestIdleCallback 如果主线程在一个帧的末尾还有任何时间,那么requestIdleCallback可以启动。这是一个做非必要工作的好机会,比如引导分析数据。如果你是新申请IdleCallback的,请在Google Developers上了解一下,它会提供更多的细分信息。

扩展

补充卡顿案例

image.png

image.png

因为使用了10张几兆的大图片做zIndex变化和一些动画导致的。