前言
其他开发者经常向我询问关于像素工作流程的部分内容,以及何时以及为什么触发某些操作,因此我觉得值得提供一个简要参考,介绍将像素呈现到屏幕上所涉及的内容
注:这是 Blink / Chrome
的视角。大多数主线程任务以某种形式被所有浏览器厂商共享,例如布局或样式计算,但整体架构可能不同
一图胜千言
将像素呈现到屏幕上的完整过程
进程
让我们更详细地定义一些内容。在这篇文章旁边有一张图片可能会对理解有所帮助,您可以在本帖旁边放置该图像,或者如果您愿意的话,可以打印出来。
让我们从这些进程开始:
-
渲染进程(Renderer Process)
:是一个标签页的周围容器。它包含多个线程,共同负责将您的页面呈现在屏幕上的各个方面。这些线程包括合成器线程(Compositor)
、切片工作线程(Tile Worker)
和主线程(Main Thread)
。 -
GPU进程(GPU Process)
:这是为所有标签页和周围的浏览器进程提供服务的单个进程。当帧被提交时,GPU进程会将任何切片和其他数据(例如四边形顶点和矩阵)上传到GPU,以实际将像素推送到屏幕上。GPU进程包含一个称为GPU线程(GPU Thread)
的单个线程来执行这项工作。
渲染进程中的线程
现在让我们来看看渲染进程中的线程。
-
合成器线程(Compositor Thread)
:这是首个收到vsync(垂直同步信号 verticalsynchronization)
事件的线程(这是操作系统告知浏览器生成新帧的方式)。它也会接收任何输入事件。如果可能的话,合成器线程将避免访问主线程,并尝试将输入(例如滚动或拖动)转化为屏幕上的移动。它通过更新图层位置并通过GPU线程直接将帧提交到GPU来实现这一点。如果由于输入事件处理程序或其他视觉工作的原因无法这样做,则需要主线程。 -
主线程(Main Thread)
:这是浏览器执行我们所熟悉和喜爱的任务的地方:JavaScript、样式、布局和绘制。(在未来的Houdini中,这将发生变化,我们将能够在合成器线程中运行一些代码)。这个线程因为大部分工作在这里执行,因此最容易导致卡顿。 -
合成器切片工作者(Compositor Tile Worker)
:由合成器线程生成的一个或多个工作者,用于处理光栅化任务。稍后我们将详细讨论这一点。
在许多方面,您应该将合成器线程视为”大老板”。虽然它不执行JavaScript、布局、绘制或其他操作,但它是完全负责启动主线程工作并将帧发送到屏幕上的线程。如果不必等待输入事件处理程序,它可以在等待主线程完成工作时发送帧。
此外,您还可以想象Service Workers
和Web Workers
存在于该进程中,但为了简化问题,我没有详细讨论它们。
主要流程
主线程全貌
让我们逐步讨论从vsync
到像素
的流程,并谈谈事物在“全功能”事件中是如何工作的。值得记住的是,浏览器不必执行所有这些步骤,这取决于所需的情况。例如,如果没有新的HTML需要解析,那么解析HTML就不会触发。**实际上,通常改进性能的最佳方法就是消除需要触发流程的部分!**
值得注意的是那些似乎指向requestAnimationFrame
的样式和布局下方的红色箭头。在您的代码中,可能会意外触发两者。这被称为强制同步布局(或样式,具体取决于情况),通常对性能不利
-
帧开始(Frame Start)
:触发垂直同步(vsync),帧开始。 -
输入事件处理程序(Input event handlers)
:输入数据从合成器线程传递到主线程上的任何输入事件处理程序。所有输入事件处理程序(如touchmove、scroll、click)应在每帧中首先触发一次,但情况并非总是如此;调度程序会尽力处理,但其成功程度因操作系统而异。用户交互和事件传递到主线程处理之间也存在一定的延迟。 -
requestAnimationFrame
:这是进行屏幕上的视觉更新的理想位置,因为您拥有新的输入数据,并且它是最接近垂直同步的时机。其他视觉任务,如样式计算,应在此任务之后执行,因此它是对元素进行变更的理想位置。如果您变更了100个类别,这不会导致100次样式计算;它们将被批量处理并稍后处理。唯一需要注意的是,不要查询任何计算样式或布局属性(如el.style.backgroundImage或el.style.offsetWidth)。如果这样做,将会提前进行重新计算样式、布局或两者,从而导致强制同步布局或更糟糕的布局抖动。 -
解析HTML(Parse HTML)
:处理任何新添加的HTML并创建DOM元素。在页面加载或执行appendChild等操作之后,您可能会看到更多此类操作。 -
重新计算样式(Recalc Styles)
:为新增或变更的元素计算样式。这可能涉及整个DOM树,也可能仅局限于发生变化的部分。例如,更改body的类可能会产生广泛影响,但值得注意的是,浏览器已经非常智能地自动限制了样式计算的范围。 -
布局(Layout)
:为每个可见元素计算几何信息(位置和尺寸)。通常会为整个文档执行布局计算,计算成本往往与DOM的大小成比例。 -
更新图层树(Update Layer Tree)
:创建堆叠上下文和深度排序元素的过程。 -
绘制(Paint)
:这是两个部分中的第一部分:绘制是为任何新的或在视觉上发生变化的元素记录绘制调用(在此处填充矩形,写入文本等)。第二部分是光栅化(见下文),在此部分执行绘制调用,并填充纹理。这部分是记录绘制调用,通常比光栅化快得多,但这两部分通常被统称为”绘制(painting)”。 -
合成(Composite)
:计算图层和切片信息,并将其返回给合成器线程进行处理。这将考虑到诸如will-change、重叠元素和任何硬件加速的画布等因素。 -
光栅计划和光栅化(Raster Scheduled and Rasterize)
:在绘制任务中记录的绘制调用现在被执行。这是在合成器切片工作者(Compositor Tile Workers)中完成的,其数量取决于平台和设备的能力。例如,在Android上,通常只有一个工作者,而在桌面上,有时可能有四个工作者。光栅化是根据图层进行的,每个图层由多个切片组成。 -
帧结束(Frame End)
:随着各个图层的切片全部光栅化,任何新的切片,以及输入数据(可能在事件处理程序中已更改),都将提交到GPU线程。 -
帧上传(Frame Ships
):最后,切片由GPU线程上传到GPU。GPU使用四边形和矩阵(通常是常规的图形库)将切片绘制到屏幕上。
奖励环节
requestIdleCallback
:如果在帧结束时主线程还有空闲时间,那么可以触发requestIdleCallback
。这是执行非必要工作的绝佳机会,比如发送分析数据。如果您对requestIdleCallback
还不熟悉,可以在Google Developers上找到一个关于它的入门指南,其中提供了更详细的介绍。
分层和合成
在工作流程中,有两个版本的深度排序出现。
首先,有层叠上下文(Stacking Contexts)
,例如如果您有两个重叠的绝对定位的div。更新图层树(Update Layer Tree)是确保处理z-index等内容的过程。
其次,有合成器图层(Compositor Layers)
,这在流程中稍后出现,并更适用于绘制元素的概念。可以通过空变换技巧(null transform hack)或will-change: transform将元素提升为合成器图层,然后可以以低成本进行变换(适用于动画!)。但是,如果存在重叠元素,浏览器可能还需要创建额外的合成器图层,以保留由z-index等指定的深度顺序。非常有趣的内容!
关于主题的发散思考
上述提到的过程几乎都在CPU上完成。只有最后一部分,即上传和移动切片的部分
,是在GPU上完成的。
然而,在Android上,当涉及到光栅化时,像素流程略有不同:GPU的使用更加频繁。绘制调用不是由合成器切片工作者进行光栅化,而是在着色器中作为OpenGL命令在GPU上执行。
这被称为GPU光栅化,它是降低绘制成本的一种方式。您可以通过在Chrome DevTools中启用FPS计量器来了解您的页面是否使用了GPU光栅化:
FPS meter
其他资源
还有很多其他内容您可能想深入了解,例如如何避免在主线程上执行工作,或者更深层次的工作原理。希望以下资源对您有所帮助:
- Compositing in Blink & WebKit 这个视频有些年头了,但仍然值得观看。
- Browser Rendering Performance Google Developers。
- Browser Rendering Performance Udacity课程(完全免费!)。
- Houdini 这是一个关于未来计划Houdini的资源,让您能够在流程的更多部分中添加更多脚本。