浏览器:帧&渲染流程

1,029 阅读7分钟

浏览器:进程与线程
浏览器:帧&渲染流程
浏览器:安全策略

一图胜千言

thread.png

进程&线程的角度

渲染进程

包裹标签页的容器。包含了多个线程,这些线程一起负责了页面显示到屏幕上的各个方面。

  • 合成线程(Compositor Thread) 。这是最先被告知垂直同步事件(vsync event,操作系统告知浏览器刷新一帧图像的信号)的线程。它接收所有的输入事件。如果可能,合成线程会避免进入主线程,自己尝试将输入的事件(比如滚动)转换为屏幕的移动。它会更新图层的位置,并经由 GPU 线程直接向 GPU 提交帧来完成这个操作。如果输入事件需要进行处理,或者有其他的显示工作,它将无法直接完成该过程,这就需要主线程了。

  • 主线程。运行js&渲染流程。主线程容易卡顿,很大程度上是因为它要做的事情太多了。

  • 合成图块栅格化线程(Compositor Tile Worker) 。由合成线程派生的一个或多个线程,用于处理栅格化任务。即将页面内容一般按照屏幕大小分块生成位图。

GPU 进程

这是一个单一的进程,为所有标签页和浏览器周边进程服务。当帧被提交时,GPU 进程会将分为图块的位图和其他数据(比如四边形顶点和矩阵)上传到 GPU 中,真正将像素显示到屏幕上。GPU 进程只有一个的线程,叫 GPU 线程,实际上是它做了这些工作。

主线程运行过程: 浏览器并不需要执行所有步骤,具体情况取决于哪些步骤是必需的

main-thread.svg

注意上图中 Recalc Styles 和 Layout 下方指向 requestAnimationFrame 的红色箭头。在代码中恰好触发这两个情况是完全可能的。这种情况叫做强制同步布局(或强制同步样式,Forced Synchronous Layout 和 Forced Synchronous Styles),就是会引起回流重绘,通常于性能不利。

  1. 开始新的一帧。垂直同步信号触发,开始渲染新的一帧图像。

  2. 输入事件的处理。从合成线程将输入的数据,传递到主线程的事件处理函数。所有的事件处理函数touchmove,scroll,click,input,Timer, window resize)都应该最先触发,每帧触发一次,但也不一定这样;具体见js三座大山之异步五基于异步的js性能优化中的事件循环与一帧的关系.

  3. requestAnimationFrame。这是更新屏幕显示内容的理想位置,因为现在有全新的输入数据,又非常接近即将到来的垂直同步信号。其他的可视化任务,比如样式计算,因为是在本次任务之后,所以现在是变更元素的理想位置。如果你改变了 —— 比如说 100 个类的样式,这不会引起 100 次样式计算;它们会在稍后被批量处理。唯一需要注意的是,不要查询进行计算才能得到的样式或者布局属性(比如 el.style.backgroundImageel.style.offsetWidth)。如果你这样做了,会导致重新计算样式,或者布局,或者二者都发生,进一步导致强制同步布局,乃至布局颠簸

  • 解析 HTML(Parse HTML) 。处理新添加的 HTML,创建 DOM 元素。在页面加载过程中,或者进行 appendChild 操作后,你可能看到更多的此过程发生。

  • 重新计算样式(Recalc Styles) 。为新添加或变更的内容计算样式。可能要计算整个 DOM 树,也可能缩小范围,取决于具体更改了什么。例如,更改 body 的类名影响可能很大,但是值得注意的是浏览器已经足够智能了,可以自动限制重新计算样式的范围。

  • 布局(Layout) 。计算每个可见元素的几何信息(每个元素的位置和大小)。一般作用于整个文档,计算成本通常和 DOM 元素的大小成比例。

  • 更新图层树(Update Layer Tree) 。这一步创建层叠上下文,为元素的深度进行排序。

  • Paint。过程分为两步:
    第一步,对所有新加入的元素,或进行改变显示状态的元素,记录 draw 调用(这里填充矩形,那里写点字);
    第二步是栅格化(Rasterization,见后文),在这一步实际执行了 draw 的调用,并进行纹理填充。Paint 过程记录 draw 调用,一般比栅格化要快,但是两部分通常被统称为“painting”。

  • 合成(Composite) :图层和图块信息计算完成后,被传回合成线程进行处理。这将包括 will-change、重叠元素和硬件加速的 canvas 等。

  • 栅格化规划(Raster Scheduled)栅格化(Rasterize):在 Paint 任务中记录的 draw 调用现在执行。过程是在合成图块栅格化线程(Compositor Tile Workers)中进行,线程的数量取决于平台和设备性能。例如,在 Android 设备上,通常有一个线程,而在桌面设备上有时有 4 个。栅格化根据图层来完成,每层都被分成块。这个过程可以通过cpu运行 也可以通过gpu运行(GPU 栅格化)是一种降低绘制(paint)成本的方法。

  • 帧结束:各个层的所有的块都被栅格化成位图后,新的块和输入数据(可能在事件处理程序中被更改过)被提交给 GPU 线程。

  • 如果还有剩余时间:浏览器会调用requestIdleCallback方法。这里可以做一些优先级不高的非UI操作的任务。

  • 发送帧:最后,但同样很重要的是,图块被上传到GPU。GPU 使用四边形和矩阵(所有常用的 GL 数据类型)将图块 draw 在屏幕上。

基于帧的工作原理,性能优化建议

1.减少js长时间执行,避免主线程被长时间占用

截屏2024-01-03 上午11.30.14.png 将这个情况转化为下面情况 截屏2024-01-03 上午11.06.38.png 或者可以利用webwork并行&requestIdleCallback空闲执行等
具体方案参考:js三座大山之异步五基于异步的js性能优化

2.避免渲染流程长时间执行,避免主线程被长时间占用

  • case1:减少布局属性更改 the-full-pixel-pipeline.jpeg 如果您更改“layout”属性,例如会更改元素几何图形(如宽度、高度或其位置)的属性(如 left 或 top CSS 属性),则浏览器需要检查所有其他元素并重排页面。任何受影响的区域都需要重新绘制,并且最终绘制的元素也需要进行合成。

  • case2:缩短渲染路径 the-pixel-pipeline-witho.jpeg 如果您更改了 CSS 中某个元素的“paint-only”属性(例如,background-imagecolor 或 box-shadow 等属性),则无需执行布局步骤即可提交对页面的视觉更新。如果可能的话,省略布局步骤,可以避免开销高昂的布局工作,否则可能导致生成下一帧时出现明显的延迟。

  • case3:让合成线程做更多的事情,减少主线程的压力
    虽然它不运行JavaScript、布局、绘制等任何工作,但它是完全负责启动主线程工作,然后将帧发送到屏幕的线程。如果它不必等待输入事件处理程序,它可以直接将帧发送到gpu。 截屏2024-01-04 下午3.39.31.png 如果您更改的属性既不需要布局也不需要绘制,则浏览器可以直接跳到合成步骤,不占用主线程。对于页面生命周期中的高压力点(例如动画或滚动),这是最便宜且最理想的方式。 the-pixel-pipeline-withou2.jpeg

3.用资源换性能

开启gpu硬件加速,占用更多的内存~

参考:

  1. the-pixel-pipeline
  2. the-anatomy-of-a-frame浏览器帧原理剖析
  3. avoid-layout-thrashing 4.inside-browser-part3