[译]浏览器一帧的剖析

原文链接:the-anatomy-of-a-frame

前言

其他开发者经常向我询问关于像素工作流程的部分内容,以及何时以及为什么触发某些操作,因此我觉得值得提供一个简要参考,介绍将像素呈现到屏幕上所涉及的内容

注:这是 Blink / Chrome 的视角。大多数主线程任务以某种形式被所有浏览器厂商共享,例如布局或样式计算,但整体架构可能不同

一图胜千言

image.png

将像素呈现到屏幕上的完整过程

进程

让我们更详细地定义一些内容。在这篇文章旁边有一张图片可能会对理解有所帮助,您可以在本帖旁边放置该图像,或者如果您愿意的话,可以打印出来。

让我们从这些进程开始:

  • 渲染进程(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 WorkersWeb Workers存在于该进程中,但为了简化问题,我没有详细讨论它们。

主要流程

image.png

主线程全貌

让我们逐步讨论从vsync像素的流程,并谈谈事物在“全功能”事件中是如何工作的。值得记住的是,浏览器不必执行所有这些步骤,这取决于所需的情况。例如,如果没有新的HTML需要解析,那么解析HTML就不会触发。**实际上,通常改进性能的最佳方法就是消除需要触发流程的部分!**

值得注意的是那些似乎指向requestAnimationFrame的样式和布局下方的红色箭头。在您的代码中,可能会意外触发两者。这被称为强制同步布局(或样式,具体取决于情况),通常对性能不利

  1. 帧开始(Frame Start):触发垂直同步(vsync),帧开始。

  2. 输入事件处理程序(Input event handlers):输入数据从合成器线程传递到主线程上的任何输入事件处理程序。所有输入事件处理程序(如touchmove、scroll、click)应在每帧中首先触发一次,但情况并非总是如此;调度程序会尽力处理,但其成功程度因操作系统而异。用户交互和事件传递到主线程处理之间也存在一定的延迟。

  3. requestAnimationFrame:这是进行屏幕上的视觉更新的理想位置,因为您拥有新的输入数据,并且它是最接近垂直同步的时机。其他视觉任务,如样式计算,应在此任务之后执行,因此它是对元素进行变更的理想位置。如果您变更了100个类别,这不会导致100次样式计算;它们将被批量处理并稍后处理。唯一需要注意的是,不要查询任何计算样式或布局属性(如el.style.backgroundImage或el.style.offsetWidth)。如果这样做,将会提前进行重新计算样式、布局或两者,从而导致强制同步布局或更糟糕的布局抖动。

  4. 解析HTML(Parse HTML):处理任何新添加的HTML并创建DOM元素。在页面加载或执行appendChild等操作之后,您可能会看到更多此类操作。

  5. 重新计算样式(Recalc Styles):为新增或变更的元素计算样式。这可能涉及整个DOM树,也可能仅局限于发生变化的部分。例如,更改body的类可能会产生广泛影响,但值得注意的是,浏览器已经非常智能地自动限制了样式计算的范围。

  6. 布局(Layout):为每个可见元素计算几何信息(位置和尺寸)。通常会为整个文档执行布局计算,计算成本往往与DOM的大小成比例。

  7. 更新图层树(Update Layer Tree):创建堆叠上下文和深度排序元素的过程。

  8. 绘制(Paint):这是两个部分中的第一部分:绘制是为任何新的或在视觉上发生变化的元素记录绘制调用(在此处填充矩形,写入文本等)。第二部分是光栅化(见下文),在此部分执行绘制调用,并填充纹理。这部分是记录绘制调用,通常比光栅化快得多,但这两部分通常被统称为”绘制(painting)”。

  9. 合成(Composite):计算图层和切片信息,并将其返回给合成器线程进行处理。这将考虑到诸如will-change、重叠元素和任何硬件加速的画布等因素。

  10. 光栅计划和光栅化(Raster Scheduled and Rasterize):在绘制任务中记录的绘制调用现在被执行。这是在合成器切片工作者(Compositor Tile Workers)中完成的,其数量取决于平台和设备的能力。例如,在Android上,通常只有一个工作者,而在桌面上,有时可能有四个工作者。光栅化是根据图层进行的,每个图层由多个切片组成。

  11. 帧结束(Frame End):随着各个图层的切片全部光栅化,任何新的切片,以及输入数据(可能在事件处理程序中已更改),都将提交到GPU线程。

  12. 帧上传(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光栅化:

image.png

FPS meter

其他资源

还有很多其他内容您可能想深入了解,例如如何避免在主线程上执行工作,或者更深层次的工作原理。希望以下资源对您有所帮助:

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY6cmFAN' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片