编辑
2025-05-07
前端
00

浏览器渲染原理是非常常见的面试问题,那么你真的了解它吗,或者说,浏览器渲染原理你可以利用起来吗。感谢之前字节的面试官,问了我一帧的流程包括哪些,我收集了一些资料,简单整理如下,之后有空再补充补充。

本文还参考了如下翻译版本:gold-miner/TODO1/the-anatomy-of-a-frame.md at master · xitu/gold-miner · GitHub

浏览器一帧渲染包括如下流程:

参与的进程包括

  • 渲染进程(Renderer Process):它是标签页的容器。包含多个线程,负责页面渲染的各个阶段。这些线程分别是:合成器线程(Compositor Thread) 、 图块工作线程(Compositor Tile Worker)和主线程(Main Thread)
  • GPU 进程(GPU Process):这是服务于所有标签页和相关浏览器进程的单一进程。提交帧时,GPU 进程会将所有图块和其他数据(例如四边形顶点和矩阵)上传到 GPU,以便将像素实际推送到屏幕。GPU 进程包含一个称为 GPU 线程的单线程,负责实际执行工作。

渲染进程中的线程

在很多方面,你可以把合成线程(Compositor Thread)看作是“总指挥”。虽然它本身不负责运行 JavaScript、布局(Layout)、绘制(Paint)等任务,但它完全负责启动主线程的相关工作,并最终将渲染好的帧展示到屏幕上。

  • 合成线程(Compositor Thread)。这是第一个接收到 vsync 事件的线程(vsync 事件是操作系统通知浏览器生成新帧的方式)。它还会接收所有输入事件。合成线程会尽量避免将任务交给主线程,而是尝试直接将输入(比如滚动的快速滑动)转换为屏幕上的移动。它通过更新图层的位置,并通过 GPU 线程将帧提交给 GPU 来实现这一点。如果由于存在输入事件处理器或其他视觉相关的工作而无法这样做,就需要主线程参与处理。
  • 主线程(Main Thread)。这是浏览器执行我们熟悉和喜爱的各种任务的地方:JavaScript、样式、布局和绘制。(未来在 Houdini 的支持下,这种情况会有所改变,届时我们可以在合成线程中运行部分代码。)由于主线程上运行的内容非常多,它也最容易引发页面卡顿,因此可以说它“最有可能导致卡顿”。
  • 合成图块工作线程(Compositor Tile Worker(s))。这是由合成线程创建的一个或多个工作线程,用于处理光栅化(Rasterization)任务。稍后我们会进一步介绍这一点。

流程概述

  1. 帧开始(Frame Start):Vsync 事件被触发,帧开始。
  2. 输入事件处理(Input event handlers):输入数据从合成线程传递到主线程的所有输入事件处理器。所有输入事件处理器(如 touchmovescrollclick)应该在每帧最先触发,但实际情况不一定总是如此;调度器会尽力安排,成功率在不同操作系统间有所不同。从用户交互到事件被主线程处理,中间还存在一定延迟。
  3. requestAnimationFrame:这是进行视觉更新的理想时机,因为你拥有最新的输入数据,并且这是最接近 vsync 的时刻。其他视觉任务,如样式计算,会在此任务之后进行,所以此时变更元素最为合适。如果你一次性变更了 100 个 class,不会导致 100 次样式计算;这些变更会被批量处理。唯一需要注意的是,不要在这时查询任何计算样式或布局属性(如 el.style.backgroundImage 或 el.style.offsetWidth)。如果这么做,会提前触发样式重计算、布局或两者,导致强制同步布局,甚至更糟的布局抖动。
  4. 解析 HTML(Parse HTML):处理新添加的 HTML,并创建 DOM 元素。页面加载期间或执行 appendChild 等操作后,这一步会频繁出现。
  5. 样式重计算(Recalc Styles):为新添加或变更的内容计算样式。可能影响整个 DOM 树,也可能仅限于部分区域,取决于变更内容。例如,修改 body 的 class 影响范围很大,但值得注意的是浏览器会自动智能地缩小样式计算的范围。
  6. 布局(Layout):为每个可见元素计算几何信息(位置和尺寸)。通常针对整个文档进行,计算量与 DOM 大小成正比。
  7. 更新 Layer树(Update Layer Tree):创建堆叠上下文并对元素进行深度排序的过程。
  8. 绘制(Paint):这是一个两步过程的第一步:绘制是为所有新出现或发生视觉变化的元素记录绘制指令(比如在这里填充矩形、在那里写文本)。第二步是光栅化(见下文),即执行绘制指令并填充纹理。绘制指令的记录通常比光栅化快很多,但这两个步骤通常统称为“绘制”。
  9. 合成(Composite):计算层和瓦片信息,并将其传递回合成线程进行处理。这一步会处理 will-change、重叠元素以及所有硬件加速的 canvas 等内容。
  10. 光栅化调度与光栅化(Raster Scheduled and Rasterize):在绘制任务中记录的绘制指令现在被执行。这一步由合成瓦片工作线程(Compositor Tile Workers)完成,线程数量取决于平台和设备能力。例如,Android 通常只有一个工作线程,桌面端有时可以有四个。光栅化是以层为单位进行的,每个层由多个瓦片组成。
  11. 帧结束(Frame End):各层的所有瓦片光栅化完成 后,任何新瓦片以及输入数据(可能已在事件处理器中发生变化)被提交到 GPU 线程。
  12. 帧提交(Frame Ships):最后但同样重要的是,GPU 线程将瓦片上传到 GPU。GPU 利用四边形和矩阵(所有常见的 GL 技术)将瓦片绘制到屏幕上。
  13. 附加环节:如果主线程在一帧结束时还有空闲时间,则会触发 requestIdleCallback。这是执行非关键性任务(如发送分析数据)的绝佳时机。

渲染流程中的层层叠加:包括两个阶段

  1. 首先是“堆叠上下文”(Stacking Contexts),比如有两个绝对定位且重叠的 div。Update Layer Tree 阶段会确保 z-index 等属性被正确处理。
  2. 其次是“合成层”(Compositor Layers),这发生在流程稍后的阶段,主要用于已绘制元素的管理。可以通过 null transform hack 或 will-change: transform 将元素提升为合成层,这样可以低成本地进行变换(适合动画)。不过,如果有重叠元素,为了保持 z-index 等属性指定的深度顺序,浏览器也可能需要创建额外的合成层。

CPU 和 GPU

  • 上述流程几乎全部在 CPU 上完成。只有最后一部分——图块的上传和移动——是在 GPU 上完成的。
  • 但在 Android 上,像素流在光栅化阶段有所不同:GPU 的使用更多。此时,不是由合成瓦片工作线程进行光栅化,而是将绘制指令作为 GL 命令在 GPU 的着色器中执行
  • 这被称为 GPU 光栅化(GPU Rasterization),是降低绘制成本的一种方式。你可以通过在 Chrome DevTools 中启用 FPS Meter 来判断页面是否启用了 GPU 光栅化。

相关文档

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:pepedd864

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!