lingxiao-Zhu / blog

总结积累,读书笔记
3 stars 0 forks source link

浏览器一帧都做了什么? #19

Open lingxiao-Zhu opened 3 years ago

lingxiao-Zhu commented 3 years ago

屏幕显示原理

首先从过去的 CRT 显示器原理说起,CRT 的电子枪从帧缓冲区获取渲染的数据,然后从上到下一行行扫描,扫描完成后显示器就呈现【一帧画面】,随后电子枪回到初始位置继续下一次扫描。

而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称 VSync。

显示器通常以固定频率进行绘制完一帧,这个刷新率就是 VSync 信号产生的频率,一般为60赫兹,每次VSync间歇是16ms。

为了屏幕连续渲染不卡顿,那么当接收到VSync信号后,CPU+GPU计算图像并将放入帧缓冲区的时间需要控制在16ms内,

不然超过16ms还没计算出来,显示器拿不到要渲染的数据,就会跳过这次,造成了掉帧

所以浏览器也需要跟上VSync的速度,计算一帧的时间控制在16ms内,达到60FPS, 60HZ ~~~ 60FPS。

image

所以赫兹和FPS是两个东西,赫兹是显示器渲染频率,物理相关;FPS对于前端来说,就是执行JS,计算样式,合成等。

浏览器每一帧的组成

浏览器的帧都集中在CPU计算上,不包含GPU部分。

image

从图上可以看出,浏览器一帧是从渲染进程中的合成(Compositor)线程接收到 VSync 信号开始的。

第一步 Frame Start

Compositor线程收到VSync信号和输入事件(input data),并且将输入事件发送到主线程。

第二步 Input event handlers

主线程处理输入事件,所有的输入事件处理程序(touchmove, scroll, click)首先触发。

一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗,浏览器会合并这些连续的事件,延迟到下一帧渲染时执行,也就是requestAnimationFrame之前。

我们的JS代码、回调、eventloop、microtasks就是在这个阶段执行,这个阶段是最容易耗时过长的,

第三步 RequestAnimationFrame

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。

第四步 Parse Html

如果DOM被修改了,就会执行这个阶段。

第五步 Reclaculate Styles

会对任何新添加或修改的内容进行计算。此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。

第六步 Layout

计算每个可见元素的几何信息(每个元素的位置和大小)。 通常是针对整个文档进行的,通常会使计算成本与DOM大小成正比。

第七步 Update Layer Tree

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

第八步 Paint

布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录。绘画记录可以看做是记录各元素绘制先后顺序的笔记。

第九步 Composite

由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上(进行分层),以便正确渲染页面;分层完成后,将图块信息传给Compositor线程。

2 到 9 步都是在主线程进行的,从图中从第六步到第九步都在Timeline中能很清楚的看见。

image

第十步 Roaster Scheduled and Rasterize

Compositor线程需要将图层切分为一块又一块的小图块(tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在GPU Process的内存中。

第十一步 Frame End

随着各个图层的图块都被栅格化、任何新图块都将和输入数据(可能已在事件处理程序中被更改)一起被提交给GPU线程,然后GPU线程将图块上传到GPU,放入到帧缓冲区等待显示器进行下一次绘制。

到这里一帧基本上就做完,如果主线程还有剩余时间的话,就会执行 requestIdleCallback 处理一些优先级不高的事情。