hello2dj

if you can't explain it simply, you don't understand it well enough

一帧一世界

原文地址

一帧的时间内到底发生了什么

一些开发者经常会问我一个问题:页面到像素渲染的流程到底是啥?以及什么时候发生渲染以及为啥。所以啊我就发现好好的解释一下渲染像素到屏幕的过程是很有必要滴,且听我细细道来

本文是从chrome/Blink的角度来谈的。其中大部分对于其他浏览器来说也是大同小异的,好比layout 或者stlye calcs, 但是总体架构可能不太一样。

俗话说一图胜千言

确实如此,那我们也从一副图开始说起吧

上图就是从得到像素到绘制到屏幕的整个完整过程

Processes

图里面有太多的东西了,所以我在下面进行了更详细的介绍,对于我们的理解来说这是很有帮助的

  1. render process. 一个tab页就是会有一个render process, 他包含了很对的thread, 他负责对我们的操作做出回应。 这些thread 包括 Compositor, Tile Worker, 以及main threads

  2. GPU process. 这个是单一的进程(就是说无论你打开多少个tab都只有这一个GPU process),他服务于所有的tab。实际渲染到屏幕的像素数据都是由那些提交到GPU process的帧中的tile数据(贴图)以及其他一些数据(例如顶点数据,矩阵数据),GPU就包含了一个thread, 而这个GPU thread才是实际干活的。

render process threads

接下来让我们看看render process中的threads

  1. Compositor thread. 当产生vsync event(vsync是指os告诉浏览器如何产生新的一帧)时这个thread是第一个被通知的。同时也会接受所有的input事件,Compositor会尽量避免打扰main thread, 他会尝试处理输入,例如处理滚动(这可不代表滚动就有compositor处理的哦)。如果他能处理,那么他会直接去更改layer的位置然后吧frames同过GPU thread 提交大GPU去, 但是如果要处理输入的事件,或者其他一个可视化的工作,那么他就会把这些交给main thread

  2. Main Thread. 这个是浏览器处理任务的thread,包括我们所熟知的和喜爱的js, styles, layout 以及 paint(这些在将来有可能会因为Houdini而发生改变,通过使用Houdini我们可以在Compositor Thread中run一些code).这个thread也或得了一个荣耀“最能引起卡顿的家伙”, 很大一部分原因是因为有太多的东西在这里运行了

  3. Compositor Tile Worker(s).这些都是由Compositor Thread启动的,是用来处理光栅化任务的(栅格化这个术语可以用于任何将向量图形转换成位图的过程)。

我们可以把Compositor Thread 当做“大boss”。因为他不去运行js,不去布局,不去paint,或者其他事情。他所要做的事情除了启动main thread, 就是把frames传输给screen. 而且如果他没有在等待input event, 他就可以在等待main thread完成任务的同时传输frames

你也可已设想 Serviec Workers 和 Web Workers 运行在this process(应该是指render process),但是我把他们放到后面在说,因为他们会使事情变得太复杂。

main thread 里的整体流程

oftentimes the best way to improve performance is simply to remove the need for parts of the flow to be fired!

让我们逐步介绍从vsync到像素的流程。 值得记住的是,浏览器不需要执行所有这些步骤,这取决于哪些是需要进行的。 例如,如果没有新的HTML解析,那么解析HTML将不会触发。 事实上,提高性能的最佳方法通常是简单地避免整个流程中某些部分的触发例如layout或者其他!

同样值得注意的是,在样式和布局下的那些红色箭头似乎指向了requestAnimationFrame。 在代码中偶然触发是完全可能的。 这称为强制同步布局(或样式),它往往不利于性能。

  1. Frame start. Vsync事件触发,一帧开始

  2. 输入事件。 输入数据同过compositor传递给main thread中相应的handler。每帧当中,首先触发的是事件处理的函数(如touchmove, scroll, click),但这不是必须的,因为有些没有事件发生。调度程序会尽力而为的尝试,成功性在不同操作系统之间有所不同。 在用户交互和事件之间还有一些延迟(making its way to the main thread to be handled 不会翻译了。。。)

  3. requestAnimationFrame. 这里是进行屏幕元素更新的理想场所,你可以在这里刷新数据,并且这里是离最近一次Vsync最近的时机。其他视觉或者可视化任务(visual tasks),例如style calcs,将在这个task之后进行,因此这里是更改元素的理想时机,如果你进行了更改了-100个classes, 将不会导致100 style calcs, 他们将会被延时批量处理。这里有一个要注意的地方就是不要在这里访问computed styles 或者是布局属性(例如el.style.backgroundImage or el.style.offsetWidth). 如果你这么做了,那么你将会引起样式的重新计算,或者是重新布局或者是全部,更甚至引起强制同步布局或者更甚至是布局恶化

  4. Parse HTML. 任何新加入的HTML都会被处理,并且创建DOM, 你经常会在页面加载时或者在类似于appendChild这样的操作后看到他

  5. Recalc Styles. 所有新加入或者改变过的样式都会被计算。这可能是整棵树,或者可以缩小范围,取决于更改的内容。 例如,更改body上的类可能是整体的,但值得注意的是,浏览器已经非常聪明地自动限制了样式计算的范围。

  6. Layout. 计算每个可见元素的几何信息(每个元素的位置和大小)。 它通常是为整个文档计算的,通常计算成本与DOM大小成比例。

  7. Update Layer Tree. 这个是给排序元素(z-index相关,overlap相关)创建层叠上下文以及深度信息的过程
    (The process of creating the stacking contexts and depth sorting elements.)

  8. Paint. 这是两部分过程中的第一个:paint是draw调用的记录(填充矩形,写入文本),以查看任何新的或视觉上已经改变的元素。 第二部分是光栅化(参见下面),绘制调用被执行,纹理被填充。这部分是绘制调用的记录,通常比光栅化要快得多,但是这两个部分通常统称为“painting”。

  9. Composite. the layer 和贴图信息被计算出来并且传递回来给compositor thread 进行处理。这是因为要处理will-chandge, 相互遮挡的元素,或者是开启了硬件加速的元素。

  10. Raster Scheduled and Rasterize: The draw calls recorded in the Paint task are now executed. This is done in Compositor Tile Workers, the number of which depends on the platform and device capabilities. For example, on Android you typically find one worker, on desktop you can sometimes find four. The rasterization is done in terms of layers, each of which is made up of tiles.

  11. Frame End: With the tiles for the various layers all rasterized, any new tiles are committed, along with input data (which may have been changed in the event handlers), to the GPU Thread.

  12. Frame Ships: Last, but by no means least, the tiles are uploaded to the GPU by the GPU Thread. The GPU, using quads and matrices (all the usual GL goodness) will draw the tiles to the screen.

Bonus round

  • requestIdleCallback: if there’s any time Main Thread left at the end of a frame then requestIdleCallback can fire. This is a great opportunity to do non-essential work, like beaconing analytics data. If you’re new to requestIdleCallback have a primer for it on Google Developers that gives a bit more of a breakdown.

LAYERS AND LAYERS

There are two versions of depth sorting that crop up in the workflow.

Firstly, there’s the Stacking Contexts, like if you have two absolutely positioned divs that overlap. Update Layer Tree is the part of the process that ensures that z-index and the like is heeded.

Secondly, there’s the Compositor Layers, which is later in the process, and applies more to the idea of painted elements. An element can be promoted to a Compositor Layer with the null transform hack, or will-change: transform, which can then be transformed around the place cheaply (good for animation!). But the browser may also have to create additional Compositor Layers to preserve the depth order specified by z-index and the like if there are overlapping elements. Fun stuff!

RIFFING ON A THEME

Virtually all of the process outlined above is done on the CPU. Only the last part, where tiles are uploaded and moved, is done on the GPU.

On Android, however, the pixel flow is a little different when it comes to Rasterization: the GPU is used far more. Instead of Compositor Tile Workers doing the rasterization, the draw calls are executed as GL commands on the GPU in shaders.

This is known as GPU Rasterization, and it’s one way to reduce the cost of paint. You can find out if your page is GPU rasterized by enabling the FPS Meter in Chrome DevTools:

OTHER RESOURCES

There’s a ton of other stuff that you might want to dive into, like how to avoid work on the Main Thread, or how this stuff works at a deeper level. Hopefully these will help you out: