Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一帧剖析 #14

Open
Godiswill opened this issue Jan 16, 2020 · 0 comments
Open

一帧剖析 #14

Godiswill opened this issue Jan 16, 2020 · 0 comments

Comments

@Godiswill
Copy link
Owner

Godiswill commented Jan 16, 2020

一帧剖析

原文链接

翻译自:The Anatomy of a Frame

经常被开发者问及像素渲染管道流的细节问题,它每一步是怎样触发的,何时触发的、为什么会触发。
这篇文章就是为了解答这些疑惑的,你将了解到像素是如何渲染到屏幕的。

注意:以下讲解基于 Blink 内核的 Chrome 浏览器。例如像layoutstyle calcs 等步骤运作在浏览器主线程中,这在大部分浏览器厂商中达成共识,但以下整体架构未必被其他厂商采纳。

一图胜千言

  • 像素到渲染到屏幕的全过程

anatomy-of-a-frame

流程

在这张图里涉及到太多的内容,可以大致分为两个大的部分。
(以下文字需要反复对照这张图,为了方便,作者很贴心的给了图片地址The Anatomy of a Frame。)

  • 渲染流程 Renderer Process

该流程包含许多线程负责处理不同的事物,确保让你的页面渲染到屏幕。
主要的线程有如下

  1. Compositor 合成线程
  2. Tile Worker 瓷砖工线程(每个像素就像瓷砖一块块叠加起来)
  3. Main threads 主线程
  • GPU流程 GPU Process

该流程服务于上述的线程和浏览器其他的进程。当渲染流程处理完帧后,将触发 commit 事件。
GPU流程将瓷砖tiles 和其他数据(例如:quads and matrices)上传给 GPUGPU 将像素输出到屏幕。
实际GPU流程包含一条单线程GPU Thread 负责与 GPU 通信。

渲染流程线程

现在分析下渲染流程中的线程。

  • 合成线程 Compositor Thread.

当用户交互例如输入框输入数据,OS触发 vsync event 首先通知 Compositor Thread
如果可以的话,该线程避免进入主线程,通过 GPU Thread 直接与 GPU 通信,把用户的交互行为渲染到屏幕上。
如果不可以(事件绑定了回调处理或其他可视化DOM操作),这时才需要主线程来处理。

  • 主线程 Main Thread

该线程执行着我们耳熟能详的工作,如JavaScript, styles, layoutpaint 等。
由于大量的工作运行在该线程中,该线程是造成页面卡顿jank的"真凶"。

  • Compositor Tile Worker(s)

Compositor Thread 会生成(spawn)一条或多条子线程 Compositor Tile Worker 来处理光栅化任务。

PS:在某种程度上 Compositor Thread 才是 big boss ,虽然它不处理 JavaScriptLayout, Paint等任务。
但它全面的控制着主线程的初始化和把每帧运输到屏幕上的工作。

其实 Service WorkersWeb Workers 也存在该过程中,只是有点复杂,暂且不表。

主线程

  • 主线程全貌(这里把Parse HTML -> Composite 之间的过程称为渲染管道流)

main-thread

接下来分析下图中完整版主线程中的每个事件是干什么工作的。
有一点需要记住的是:在实际中并不是每一步都会执行。
例如没有新的 HTML 需要解析,Parse HTML 就不会执行。
前面提到的卡顿的优化,就是避免渲染管道中的事件反复执行或避免某部分的执行。
具体参考渲染性能优化的最佳实践课程。

同样值得注意的是:styleslayout 底下的红色箭头指向 requestAnimationFrame
requestAnimationFrame 能确保在一帧(其实是指渲染管道流)开始前执行完毕。
但如果你在 requestAnimationFrame 代码不注意的话,可能会触发styles,layout的提前执行,
导致强制同步重排/布局 Forced Synchronous Layout,极大的破坏页面性能。

PS:以前的一些库动画实现例如 jQuery 依赖 setTimeoutsetInterval,可以的话尽量用 CSS3requestAnimationFrame 替代。

  1. Frame StartVsync 触发, 一帧开始。
  2. Input event handlers :合成线程 compositor threadinput 数据传给主线程,
    处理事件回调。OS调度程序将尽最大努力合理调度事件回调(touchmove, scroll, click等),以及时响应用户交互。
    即便如此,在用户交互和主线程处理事件获得响应之间多少会有些延迟。
  3. requestAnimationFrame
    因为它离vsync很近,可以就近获取input data。所以这是操作dom理想的地方,例如修改 100 个元素的 class
    并不会导致100 次样式计算style calculations,而是会在之后的管道流中批量处理。
    需要注意的是:你不能在查询任何已计算的样式或布局属性(例如el.style.backgroundImage, el.style.offsetWidth)。
    如果你这么做了,就像图中红色箭头标明的一样,recalc styleslayout 或两者会提前执行,将导致强制布局,更糟会引起页面抖动。
    Avoid Large, Complex Layouts and Layout Thrashing
  4. Parse HTML:任何新增的 HTML 都会被处理,构建新的DOM元素。
    你可以在页面加载或 appendChild 等操作中看到这一过程。
  5. Recalc Styles:对于解析样式文件或classstyle 等样式操作都会引发样式计算。可能会重新渲染整棵样式树。
    具体取决于哪个元素的样式改变,例如 body 就影响比较大。值得注意的是,浏览器已经很聪明能自动限制波及的范围。
  6. Layout:计算可见元素几何(盒模型)信息(位置、尺寸)。通常会对整个文档操作一遍。
    产生的开销和 DOM 个数成正比例关系。
  7. Update Layer Tree:创建层叠上下文和元素层级顺序。
  8. Paint:其实这是绘画的第一步,这一步记录需要调用绘制的方法 draw calls (fill a rectangle here, write text there)。
    第二步是光栅化 Rasterization (下面会提到),draw calls 会被执行。
    第一步显然速度要快于第二步,但经常把这两步都成为 painting
  9. Composite:层 layer 和瓷砖 tile 信息被计算后回传给 compositor thread 处理。
    Composite 负责处理有 will-changeoverlapping elements,或任何硬件加速的 canvas
  10. Raster Scheduled and Rasterize:光栅调度和光栅化,这里将执行在 Paint 任务中提到的draw calls
    将在 Compositor Tile Workers 中处理,该线程的多少取决于系统和硬件设备的能力。
    例如 Android 通常起一个Compositor Tile Workers线程,PC 可能有4个。
    根据层 layers 信息来光栅化,layers 是由很多瓷砖 tiles 组成。
  11. Frame End:所有 layers 中被光栅化的 tilesinput data (可能被事件回调处理了)将被提交给 GPU Thread
  12. 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 层级的方法

  1. will-change: transform /* transform 不支持的 hack */
  2. transform: translateZ(0) /* 3D */
    对于层级少 animation 元素能减少性能开销(避免主线程管道流中的某部分执行),俗称 GPU加速
    但浏览器可能不得不创建额外的 Compositor Layers 来保存层叠顺序(z-index指定),
    这就是产生了 overlapping elements 元素。

发散主题 RIFFING ON A THEME

上述主线程描述的过程差不多都发生在CPU中。只有最后一步,传输完后 tilesGPU中处理。

然而在 Android 中有点不同,Compositor Tile Workers 光栅化 Rasterization 的操作,也在 GPU 中完成。
draw calls 作为 GL 命令在 GPU 着色器中执行。

这被称为 GPU Rasterization,这是一种减少 paint 成本的一种方式。
如果页面使用了GPU光栅化,能在Chrome DevTools 中的 FPS Meter 查看。

  • FPS Meter

FPS Meter

参考

如果你想学习如何避免卡顿 jank,想对性能优化方面有更高级的认知,
下面这些都能帮助你。

  1. Compositing in Blink & WebKit.A little old now, but still worth a watch.
  2. Browser Rendering Performance - Google Developers.
  3. Browser Rendering Performance - Udacity Course (totally free!).
  4. Houdini - The future, where you get to add more script to more parts of the flow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant