一帧剖析
Godiswill opened this issue · comments
一帧剖析
经常被开发者问及像素渲染管道流的细节问题,它每一步是怎样触发的,何时触发的、为什么会触发。
这篇文章就是为了解答这些疑惑的,你将了解到像素是如何渲染到屏幕的。
注意:以下讲解基于 Blink
内核的 Chrome
浏览器。例如像layout
或 style calcs
等步骤运作在浏览器主线程中,这在大部分浏览器厂商中达成共识,但以下整体架构未必被其他厂商采纳。
一图胜千言
- 像素到渲染到屏幕的全过程
流程
在这张图里涉及到太多的内容,可以大致分为两个大的部分。
(以下文字需要反复对照这张图,为了方便,作者很贴心的给了图片地址The Anatomy of a Frame。)
- 渲染流程 Renderer Process
该流程包含许多线程负责处理不同的事物,确保让你的页面渲染到屏幕。
主要的线程有如下
- Compositor 合成线程
- Tile Worker 瓷砖工线程(每个像素就像瓷砖一块块叠加起来)
- Main threads 主线程
- GPU流程 GPU Process
该流程服务于上述的线程和浏览器其他的进程。当渲染流程处理完帧后,将触发 commit
事件。
GPU流程将瓷砖tiles
和其他数据(例如:quads and matrices)上传给 GPU
,GPU
将像素输出到屏幕。
实际GPU流程包含一条单线程GPU Thread
负责与 GPU
通信。
渲染流程线程
现在分析下渲染流程中的线程。
- 合成线程 Compositor Thread.
当用户交互例如输入框输入数据,OS触发 vsync event
首先通知 Compositor Thread
。
如果可以的话,该线程避免进入主线程,通过 GPU Thread
直接与 GPU
通信,把用户的交互行为渲染到屏幕上。
如果不可以(事件绑定了回调处理或其他可视化DOM操作),这时才需要主线程来处理。
- 主线程 Main Thread
该线程执行着我们耳熟能详的工作,如JavaScript
, styles
, layout
和 paint
等。
由于大量的工作运行在该线程中,该线程是造成页面卡顿jank
的"真凶"。
- Compositor Tile Worker(s)
Compositor Thread
会生成(spawn)一条或多条子线程 Compositor Tile Worker
来处理光栅化任务。
PS:在某种程度上 Compositor Thread
才是 big boss
,虽然它不处理 JavaScript
、Layout
, Paint
等任务。
但它全面的控制着主线程的初始化和把每帧运输到屏幕上的工作。
其实 Service Workers
,Web Workers
也存在该过程中,只是有点复杂,暂且不表。
主线程
- 主线程全貌(这里把Parse HTML -> Composite 之间的过程称为渲染管道流)
接下来分析下图中完整版主线程中的每个事件是干什么工作的。
有一点需要记住的是:在实际中并不是每一步都会执行。
例如没有新的 HTML
需要解析,Parse HTML
就不会执行。
前面提到的卡顿的优化,就是避免渲染管道中的事件反复执行或避免某部分的执行。
具体参考渲染性能优化的最佳实践课程。
同样值得注意的是:styles
和 layout
底下的红色箭头指向 requestAnimationFrame
。
requestAnimationFrame
能确保在一帧(其实是指渲染管道流)开始前执行完毕。
但如果你在 requestAnimationFrame
代码不注意的话,可能会触发styles,layout
的提前执行,
导致强制同步重排/布局 Forced Synchronous Layout
,极大的破坏页面性能。
PS:以前的一些库动画实现例如 jQuery
依赖 setTimeout
或 setInterval
,可以的话尽量用 CSS3
和 requestAnimationFrame
替代。
Frame Start
:Vsync
触发, 一帧开始。Input event handlers
:合成线程compositor thread
把input
数据传给主线程,
处理事件回调。OS调度程序将尽最大努力合理调度事件回调(touchmove, scroll, click等),以及时响应用户交互。
即便如此,在用户交互和主线程处理事件获得响应之间多少会有些延迟。requestAnimationFrame
因为它离vsync
很近,可以就近获取input data
。所以这是操作dom
理想的地方,例如修改100
个元素的class
,
并不会导致100
次样式计算style calculations
,而是会在之后的管道流中批量处理。
需要注意的是:你不能在查询任何已计算的样式或布局属性(例如el.style.backgroundImage
,el.style.offsetWidth
)。
如果你这么做了,就像图中红色箭头标明的一样,recalc styles
或layout
或两者会提前执行,将导致强制布局,更糟会引起页面抖动。
Avoid Large, Complex Layouts and Layout Thrashing。Parse HTML
:任何新增的HTML
都会被处理,构建新的DOM
元素。
你可以在页面加载或appendChild
等操作中看到这一过程。Recalc Styles
:对于解析样式文件或class
或style
等样式操作都会引发样式计算。可能会重新渲染整棵样式树。
具体取决于哪个元素的样式改变,例如body
就影响比较大。值得注意的是,浏览器已经很聪明能自动限制波及的范围。Layout
:计算可见元素几何(盒模型)信息(位置、尺寸)。通常会对整个文档操作一遍。
产生的开销和DOM
个数成正比例关系。Update Layer Tree
:创建层叠上下文和元素层级顺序。Paint
:其实这是绘画的第一步,这一步记录需要调用绘制的方法draw calls
(fill a rectangle here, write text there)。
第二步是光栅化Rasterization
(下面会提到),draw calls
会被执行。
第一步显然速度要快于第二步,但经常把这两步都成为painting
。Composite
:层layer
和瓷砖tile
信息被计算后回传给compositor thread
处理。
Composite
负责处理有will-change
,overlapping elements
,或任何硬件加速的canvas
。Raster Scheduled and Rasterize
:光栅调度和光栅化,这里将执行在Paint
任务中提到的draw calls
。
将在Compositor Tile Workers
中处理,该线程的多少取决于系统和硬件设备的能力。
例如Android
通常起一个Compositor Tile Workers
线程,PC 可能有4个。
根据层layers
信息来光栅化,layers
是由很多瓷砖tiles
组成。Frame End
:所有layers
中被光栅化的tiles
和input data
(可能被事件回调处理了)将被提交给GPU Thread
。Frame Ships
:最后,所有的瓷砖tiles
都将被GPU Thread
上传给硬件GPU
处理。
GPU
将使用quads and matrices
来把tiles
打印在屏幕上。
- 意外收获:
requestIdleCallback
在主线程处理完一帧后还剩余一些处理空间的话 requestIdleCallback
会被触发。
这是一个非常好的机会来处理非必要的工作,如用户行为信息等采集。
如果你是一个新手,这里有份参考:Using requestIdleCallback。
值得一提,最新的 React
利用此api来优化性能。
LAYERS AND LAYERS
上面流程提及 layer
有两中概念,以示区分。
- Update Layer Tree 中的 Layer
层叠上下文 the Stacking Contexts
。例如两个绝对定位的div
,根据元素前后出现和z-index
会产生层叠关系。
这一过程在 Update Layer Tree
中处理,确保正确的层叠顺序。
- Compositor Layers 中的 Layers
这在之后的流程中处理,更适用于已绘制的元素的概念。
提升元素 Compositor Layer
层级的方法
will-change: transform
/* transform 不支持的 hack */transform: translateZ(0) /* 3D */
对于层级少animation
元素能减少性能开销(避免主线程管道流中的某部分执行),俗称GPU加速
。
但浏览器可能不得不创建额外的Compositor Layers
来保存层叠顺序(z-index指定),
这就是产生了overlapping elements
元素。
发散主题 RIFFING ON A THEME
上述主线程描述的过程差不多都发生在CPU
中。只有最后一步,传输完后 tiles
在 GPU
中处理。
然而在 Android
中有点不同,Compositor Tile Workers
光栅化 Rasterization
的操作,也在 GPU
中完成。
draw calls
作为 GL 命令在 GPU
着色器中执行。
这被称为 GPU Rasterization
,这是一种减少 paint
成本的一种方式。
如果页面使用了GPU光栅化,能在Chrome DevTools
中的 FPS Meter
查看。
- FPS Meter
参考
如果你想学习如何避免卡顿 jank
,想对性能优化方面有更高级的认知,
下面这些都能帮助你。
- Compositing in Blink & WebKit.A little old now, but still worth a watch.
- Browser Rendering Performance - Google Developers.
- Browser Rendering Performance - Udacity Course (totally free!).
- Houdini - The future, where you get to add more script to more parts of the flow.