hushicai.github.io icon indicating copy to clipboard operation
hushicai.github.io copied to clipboard

浏览器每一帧

Open hushicai opened this issue 6 years ago • 17 comments

浏览器在每一帧中都做了哪些事情? 何时以及为什么会触发? 如何将像素呈现至屏幕上?

注意:本文是从Chrome/Blink的角度看待问题的!

A Picture Speaks A Thousand Words

image

Process

让我们先从进程开始说起。

1. Renderer Process

浏览器一个标签页的周边容器。 它包含多个线程,它们共同负责在屏幕上显示页面的各个方面。 这些线程是 CompositorTile Worker,和 Main Thread

2. GPU Process

这是为浏览器所有标签页和周边进程提供服务的单个进程。 随着帧的提交,GPU进程会将任何图块和其他数据(如四边形顶点和矩阵)上传到GPU,以实际将像素推送到屏幕。 GPU进程包含一个单独的线程,称为GPU线程,它实际上完成了工作。

Renderer Process Threads

现在让我们看一下渲染进程中的线程。

1. Compositor Thread

这是第一个被告知vsync事件的线程(这是操作系统告诉浏览器创建新帧的方式)。 它还将接收任何输入事件。 如果可以的话,Compositor Thread将避免进入Main Thread,并尝试将输入(例如滚动)转换为屏幕上的移动。 它将通过更新层位置并通过GPU线程直接将帧提交到GPU来实现。 如果由于输入事件处理程序或其他可视化工作而无法执行此操作,则需要将工作提交给Main Thread

2. Main Thread

这是浏览器执行我们都知道和喜爱的任务的地方:JavaScript,样式,布局和绘画。(这将在Houdini的未来发生变化,我们将能够在Compositor Thread中运行一些代码。) 这个线程赢得了“最有可能导致卡顿”的奖励,主要是因为这里运行了太多任务。

3. Compositor Tile Worker(s)

Compositor Thread生成的一个或多个工作程序,用于处理栅格化任务,我们稍后会详谈。

在许多方面,您应该将Compositor Thread视为“大老板”。 虽然它不运行JavaScript,Layout,Paint或其中任何一个,但它是完全负责初始化 Main Thread工作,然后将帧传送到屏幕。 如果它不必等待输入事件处理程序,它可以在等待Main Thread完成其工作时,直接发送帧。

您也可以认为Service WorkersWeb Workers存活在这个进程中,尽管我将它们排除在外,因为它使事情变得更加复杂。

The Flow Of Things

image

让我们逐步完成从vsync到像素的流程,并讨论事情在Main Thread上是如何运作的。 值得记住的是,浏览器无需执行所有这些步骤,具体取决于所需的步骤。 例如,如果没有要解析的新HTML,则不会触发Parse HTML。 实际上,通常提高性能的最佳方法就是简单地消除对部分流程的需求!

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

1. Frame Start

Vsync事件触发后, 一个帧开始了。

2. Input event handlers

输入数据从Compositor Thread传递到Main Thread上的任何输入事件处理程序。 所有输入事件处理程序(touchmove,scroll,click)应首先触发,每帧一次,但不一定是这样, 调度程序会尽力尝试,其成功因操作系统而异。 从用户交互到Main Thread处理事件,这中间也存在一些延迟。

3. requestAnimationFrame

这是对屏幕进行可视更新的理想场所,因为您有新的输入数据,并且它与您将获得的vsync事件很接近。 其他视觉任务,如样式计算,将在此任务之后完成,因此这里是修改元素的理想时机。 如果你改变 - 比方说 - 100个class,这将不会导致100个样式计算;它们将被批量处理并稍后处理。 唯一需要注意的是,您不要查询任何计算样式或布局属性(如el.style.backgroundImage或el.style.offsetWidth)。 如果你这样做,将会触发重新计算样式,布局或两者,再者,它会导致强制同步布局或者更糟糕的布局抖动

4. Parse HTML

任何新添加的HTML都需要被处理,并创建DOM元素。 在页面加载期间或者之后诸如appendChild等操作,您都有可能看到很多这样的内容。

5. Recalc Styles

任何新添加或修改的内容都需要重新计算样式。 这可能是整棵树,或者它可以缩小范围,具体取决于更改的内容。 例如,更改body上的class可能是深远的,但值得注意的是,浏览器已经可以非常聪明地自动限制样式计算的范围。

6. Layout

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

7. Update Layer Tree

创建堆叠上下文和深度排序元素的过程。

8. Paint

这是两部分过程中的第一部分:绘画是绘制调用的记录(例如在这里填充一个矩形,在那里写文本),用于任何新建的或者视觉发生变化的元素。 第二部分是栅格化(见下文),执行绘制调用,填充纹理。 这部分是绘制调用的记录,通常比栅格化快得多,但这两部分通常统称为“painting”。

9. Composite

计算图层和图块信息并将其传递回Compositor Thread以供其处理。 除其他外,这将解决诸如will-change,重叠元素和任何硬件加速画布之类的事情。

10. Raster Scheduled and Rasterize

Paint任务中记录的绘制调用会在这里执行。 这在 Compositor Tile Workers 中完成,其数量取决于平台和设备功能。 例如,在Android上,您通常会找到一个工作者,在桌面上您有时可以找到四个。 栅格化是根据图层完成的,每个图层都由图块组成。

11. Frame End

随着各个图层的图块都被栅格化、任何新图块都将和输入数据(可能已在事件处理程序中被更改)一起被提交给GPU线程。

12. Frame Ships

最后,但绝不是最不重要的,GPU线程将图块上传到GPU。 GPU,使用四边形和矩阵将图块绘制到屏幕上。

13. requestIdleCallback

如果Main Thread在帧结束时有空余时间,那么requestIdleCallback可以触发。 这是进行非必要工作的绝佳机会,例如收集统计数据。 如果您是刚接触requestIdleCallback,可以在Google Developers上了解一下,它提供了更多细节。

Reference

  • https://aerotwist.com/blog/the-anatomy-of-a-frame/

hushicai avatar Jun 29 '18 16:06 hushicai

输入事件处理程序示意图: image

hushicai avatar Jul 31 '18 05:07 hushicai

浏览器渲染流程 详细分析

这篇文章涵盖内容比较多,主要是demo讲得够详细,挺不错。

hushicai avatar Sep 17 '18 00:09 hushicai

🐂 🍺

HalloAlex avatar Mar 26 '21 08:03 HalloAlex

不好意思啊大佬,我有几个关于FPS和帧的问题== 我打开performance看,页面渲染整个过程中,FPS基本是从1到60螺旋式递增的。我就有了几个疑问:

  1. 当FPS为1时,是不是意味着设备的刷新率也是1hz啊?
  2. 设备刷新率其实跟FPS是两个概念?
  3. 页面渲染过程中,分层合成时最终会生成一个合成帧,FPS中的Frame是指这个合成帧吗?如果不是合成帧,帧又是啥呢==
  4. 一个帧的计时是怎么样的哇?

EightCanisters avatar Apr 23 '22 23:04 EightCanisters

在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,其中帧是指每一张画面。

如果你有很多张连续的图片,以60 fps快速播放,基本就跟放电影似的,那么这里一张图片就是一帧。

浏览器也是类似,每次渲染,最终都是每一阵提供一张图片给系统底层渲染机制进行显示。

浏览器的FPS跟环境息息相关,有可能进程繁忙,有可能任务繁重等等,跟设备刷新率不是一回事。

hushicai avatar Apr 29 '22 01:04 hushicai

谢谢大佬!!!

| | 黄林秀 前端开发工程师 | | 18428331060 @.*** |

---- 回复的原邮件 ---- | 发件人 | @.> | | 日期 | 2022年04月29日 09:01 | | 收件人 | @.> | | 抄送至 | @.@.> | | 主题 | Re: [hushicai/hushicai.github.io] 浏览器每一帧 (#5) |

在视频领域,电影、电视、数字视频等可视为随时间连续变换的许多张画面,其中帧是指每一张画面。

如果你有很多张连续的图片,以60 fps快速播放,基本就跟放电影似的,那么这里一张图片就是一帧。

浏览器也是类似,每次渲染,最终都是每一阵提供一张图片给系统底层渲染机制进行显示。(基本就是这个这)

浏览器的FPS跟环境息息相关,有可能进程繁忙,有可能任务繁重等等,跟设备刷新率不是一回事。

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

EightCanisters avatar Apr 29 '22 01:04 EightCanisters

您提到在rAF里面不要查询任何计算布局..... 但是在js中得到的应该是上一帧的布局信息吧,那为什么又会 forces layout / reflow呢...

RCQE avatar Jul 20 '23 07:07 RCQE

您提到在rAF里面不要查询任何计算布局..... 但是在js中得到的应该是上一帧的布局信息吧,那为什么又会 forces layout / reflow呢...

不是不要查询,是不建议先修改再查询。

建议自行了解一下forced sync layout的具体触发条件哈。

hushicai avatar Jul 23 '23 03:07 hushicai

image 这个图里面没有看到js事件循环执行时机,事件循环是在input event handlerrequestAnimationFrame之间吗?

0-Captain avatar Aug 27 '23 02:08 0-Captain

image 这个图里面没有看到js事件循环执行时机,事件循环是在input event handlerrequestAnimationFrame之间吗?

这里是讨论每一帧的任务,跟事件循环没有太大关系。 事件循环类似这样: image 不断地执行tasks,然后有v-sync信号来了,就进入一个帧的执行任务。 如果非要关联起来,可以认为事件循环是主任务,然后等v-sync信号来了,就进入每一帧的子任务。 而这些所有的任务都是在一个单线程main thread中完成,直到调用栈为空,就调度下一个task。

hushicai avatar Oct 10 '23 07:10 hushicai