大家好,我是柒八九
。好久没有更文了(2周),一来是项目活比较多,二来空余时间在系统学习其他的东西,现在还未达到写文章总结的阶段,先做一个剧透,是关于WebAssembly
和Vue3原理
的,后期会有一些列总结和教程。👉 「敬请期待」。
所以总而言之,最近更文懈怠了。
但是,但是,但是,转折来了。今天给大家带来了一个关于Chromium
最新「渲染架构」 RenderNG
的译文。(其实这是一些列文章中一篇,后期也会有另外文章的择重翻译)。
在V8如何处理JS的文章中,我们简短的介绍过浏览器的发展历史,并且还有几个奇怪的知识点。
既然,Chromium
和Chrome
之间存在不清不楚的关系。所以,针对Chromium
的研究其实就是对Chrome
后续最新技术方向的尝鲜。毕竟,Chrome
在当前浏览器份额中一家独大。掌握了它,就相当于掌握了,浏览器最新技术的发展脉络。
「last but not least」,也不知道大家在平时页面开发之余,是否对浏览器渲染页面的「内部机制」产生过兴趣。如果有的话,想必大家肯定查阅过很多资料。
例如:(如果连接失效,请在文末查找)
文章地址 | 年份 | 推荐⭐️数 |
---|---|---|
How Browsers Work | 2011 | ⭐️ |
Inside look at modern web browser | 2018 | ⭐️⭐️⭐️ 需要🪜 |
Life of a Pixel | 2020 | ⭐️⭐️⭐️⭐️有点晦涩难懂需要🪜 |
页面是如何生成的(宏观角度) | 2022 | ⭐️⭐️⭐️⭐️⭐️ |
❝其实页面是如何生成的(宏观角度)是参考各种资料的一个汇总,也算是自我总结。然后,见文知意,该篇文章是从「宏观角度」讲述了浏览器是如何处理页面的。 ❞
而这篇文章的原文是负责Blink
中渲染引擎研发的主管所写。无论是从专业角度和时间新鲜程度(2021年)都「墙裂推荐」。
同时,为了行文方便,并且这也算是知识的「二次加工」,此篇文章不会原封不动的进行机械式翻译。会有一些其他资料的补充和修改。望周知。如果想看原文,文章最后会保有连接。(但是需要🪜)
frame
)frame
结构包含:
- DOM 数据信息
- CSS
- 画布信息Canvas
- 「其他资源」,例如图片image/视频video/字体font/SVG等Animate
)开启「硬件加速」RenderNG 渲染流程
高屋建瓴的对「渲染流程」做一个归纳的话,其实它兼顾了四个「方向」:
❝每个「tab」中被渲染的页面内容是一个「树形结构」的数据格式(
frame
) 其中包含浏览器自带的UI(例如:输入栏、书签栏等) ❞
输入事件input event
的事件流来源很多,例如触摸屏幕、键盘输入还有其他的硬件设备。
❝每一个
frame
结构包含: - DOM 数据信息 - CSS -画布信息Canvas - 其他资源,例如图片image/视频video/字体font/SVG等 ❞
「一个frame
结构代表一个HTML文档(包含URL信息)」
被浏览器加载的 Web 应用,存在一个「顶层frame」,还有若干子frame。(如果有的话)。
「视觉效果」是一种应用于位图bitmap的图形操作,例如常规的滚动scroll
/剪切clip
/转换transform
/过滤filter
/透明处理opacity
/混合blend
渲染流程
可以将渲染流程想象成一个拥有很多关键节点的「流水线操作模式」。在每一个节点都会对来自上一个节点的“原料”进行深度加工,最终会将初始原料HTML
文档渲染成屏幕中的图像信息。(每个关键节点中都会有自己特定的数据格式,这个我们会有一篇文章介绍)
❝渲染过程主要涉及到 1. 渲染进程中的主线程 2. 渲染进程中的合成线程 3. viz进程(也叫GPU进程) ❞
在渲染流程的图中,用不同颜色来标识该阶段可能会被不同的线程或者进程所执行。
颜色 | 所在进程/线程 |
---|---|
绿色 | 渲染进程中的主线程 |
黄色 (黄色) | 渲染进程中的合成线程 |
橘色 | viz进程(也叫GPU进程) |
在某些阶段,可能会被多个地方所执行,所以该阶段可能存在多个颜色。
然后,我们来简单介绍一下每个节点:
computed style
或者修改「属性树」property tree
,然后开启GPU的「硬件加速」
例如:使用一些CSS3的属性
1. transform
2. opacity
3. filter
4. will-change
.element {
transform: rotateZ(360deg);
transform: translate3d(0, 0, 0);
}
computed style
)size
)和位置(position
)
👉 并且生成「不可变fragment树」 (immutable fragment tree
)
👉 「非可视化」的 DOM 元素不会插入布局树中
例如“head”元素/如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)property trees
)并且「酌情」使任何现有的显示列表(display list)和GPU纹理瓦片失效。
👉 生成「属性树」(property trees
)display list
)用于描述如何从DOM中栅格化(raster
) GPU纹理瓦片
👉 生成「显示列表」(display list
)composited layer list
),用于独立的栅格化(rasterization
)和动画制作
👉关键数据 「图层列表」compositor frame
),代表如何绘制和定位GPU纹理到屏幕,以及相关的视觉效果
👆🏻 上面大部分在渲染进程的合成线程中11.合成Aggregate:将「所有」可见合成frame的合成 frame 合并为一个「单一的、全局的」合成器frame。12. 绘制Draw:在GPU上执行聚合的合成frame,在屏幕上创建像素。
👆🏻 上面大部分在Viz进程中
在渲染流程中,有些阶段是可以被跳过的。例如:动画、滚动等可以跳过 布局、重绘、和绘制阶段
。这也解释了在渲染流程图中动画(animate)/滚动(scroll)阶段存在两个颜色(绿色/黄色)。
❝如果针对某些可视化效果,能够跳过布局、重绘和绘制阶段,那么它们可以「直接」跳过渲染主线程然后运行在合成线程中。也就是我们常说的 「硬件加速」。 ❞
多个CPU进程的使用实现了「站点之间和浏览器状态的性能和安全隔离」,以及与GPU硬件的稳定性和安全隔离。
进程关系
通常,tab和渲染进程是「一一对应」的,也就是说一个tab会有属于自己的渲染进程。但是,如果多个tabs之间属于「同一站点」并且 A 页面「打开」了 B页面。此时 A/B是「共用」一个渲染进程的。具体介绍,可以看之前写的文章。页面是如何生成的(宏观角度)
「整个Chromium中只存在一个Viz 进程」。毕竟,通常只有一个GPU和屏幕可供绘制。
由于浏览器可以有很多标签和窗口,而且都有浏览器UI像素需要绘制,你可能会问:为什么只有一个浏览器进程?
原因是:
❝「在同一时刻只有被唤起的页面才会占用浏览器进程」 ❞
事实上,「不可见的浏览器标签大多被停用,并丢掉所有的GPU内存」。
❝线程有助于实现「管道并行化」和「多重缓冲」 ❞
渲染进程中的线程分类
event loop
)、
3. 负责文档生命周期
4. 「脚本事件调度」
5. 「解析」HTML、CSS和其他数据格式将主线程和合成器线程分开,对于将动画和滚动与主线程工作的性能隔离至关重要。
「每个渲染进程只有一个主线程」,即使同一网站的多个标签或frame
可能最终出现在同一进程中。然而,在各种浏览器API中执行的工作是有性能隔离的。例如,Canvas API中图像位图和Blobs的生成在一个主线程辅助线程中运行。
同样地,「每个渲染进程只有一个合成器线程」。一般来说,只有一个并不是问题,因为合成器线程上所有「真正昂贵的操作」都被委托给合成器工作线程或Viz进程,而且这些工作可以与输入路由、滚动或动画「并行进行」。
「合成器工作线程的数量取决于设备的能力」。例如,台式机一般会使用更多的线程,因为它们有更多的CPU内核,而且比移动设备的电池限制要少。
❝渲染过程的「线程架构」是三种不同优化模式的应用。 - 「辅助线程」:将「耗时任务」的子任务发送给其他线程,以保持父线程对同时发生的其他请求的响应。主线程的辅助线程和合成器的辅助线程是这种技术的好例子。 - 「多重缓冲」:在渲染新内容的同时显示以前渲染的内容,以「隐藏渲染的延迟」。合成器线程使用这种技术。同样的我们在页面是如何生成的(宏观角度)中的双缓存中介绍过此类技术细节。 - 「管线并行化」:在多个地方「同时运行」渲染管线。这就是为什么滚动和动画可以很快,即使主线程的渲染更新正在发生,因为滚动和动画可以并行运行。 ❞
浏览器进程架构
❝浏览器进程的渲染和合成线程与渲染进程的代码和功能类似,只是主线程和合成器线程被合并为一个 ❞
Viz 进程架构
frame
合并成一个并绘制到屏幕上。frame
,以便向屏幕展示。「栅格化」和「页面绘制」通常发生在同一个线程上,因为它们都依赖于GPU资源,而且很难可靠地多线程使用GPU。也就是它们都位于GPU主线程。
显示合成器是在一个不同的线程上,因为它需要在「任何时候」都有反应,并且不阻塞任何可能导致GPU主线程变慢的来源。导致GPU主线程速度变慢的一个原因是对非Chromium代码的调用,例如供应商特定的GPU驱动程序,这些代码可能以难以预测的方式变慢。
在每个渲染过程主线程或合成器线程中,都有一些「逻辑组件」,它们以结构化的方式相互作用。
frame
树和frame内的DOMlocal frame tree
frame树是指主页面和它的子iframe,是从主页面递归生成的。
❝如果一个frame是在一个渲染进程中渲染的,那么它就是该进程的「本地框架」,否则就是「远程框架」。 ❞
根据帧的渲染过程为其着色。在前面的图片中,绿色的圆圈是一个渲染过程中的所有帧;红色的是第二个,而蓝色的是第三个。
「一个局部框架树local frame tree是框架树中相同颜色的连接组件」。图片中共有四个局部框架树:两个用于站点A,一个用于站点B,一个用于站点C,
❝「每个局部框架树local frame tree都有自己的Blink渲染器组件」 ❞
一个局部框架树的Blink渲染器可能与其他局部框架树处于同一渲染过程中,也可能不在同一渲染过程中。
存在如下结构的文档信息。包含三个标签页(foo/bar/baz
)。
<html>
<iframe id=one src="foo.com/other-url"></iframe>
<iframe id=two src="bar.com"></iframe>
</html>
foo.com
引用了自身文档中的子页面 和另外一个标签页(bar.com
)
<html>
…
</html>
<html>
…
</html>
标签页对应的进程、线程和组件结构关系
在文章开头讲过,「渲染流程」兼顾了四个「方向」:
然后我们通过几个例子来讲解一下,它们是如何实现的。
❝该过程涉及多个iframe 1. 主frame => foo.com 2. bar.com 所以涉及到了多个合成帧的融合处理 ❞
foo.com
页面渲染过程中,主线程Main Thread中某个脚本script修改了部分DOM结构foo.com
、bar.com
和浏览器UI的渲染帧「合并」成一个合成帧❝只涉及到一个页面:即
bar.com
❞
bar.com
渲染过程的合成器线程在其事件循环运行器event loop runner中通过「突变」现有的属性树property trees来触发一个动画。然后「重新运行」合成器的生命周期。bar.com
和浏览器UI的渲染帧「合并」成一个合成帧❝只涉及到一个页面:即
baz.com
❞
baz.com
的渲染进程合成器线程compositor threadpointerdown、touchstar、pointermove、touchmove或wheel
),看监听器是否会在事件上调用preventDefault
preventDefault
来决定将事件返回给合成器bar.com
渲染进程的「合成器线程」在其合成器事件循环compositor event loop中触发了一个动画。然后在属性树property tree中改变滚动偏移,并「重新运行」合成器生命周期。它还告诉主线程启动一个滚动事件baz.com
和浏览器UI的渲染帧「合并」成一个合成帧❝在
bar.com
中执行click
事件 ❞
bar.com
对应的渲染进程应该接收该点击事件,并将其发送到那里bar.com
的合成器线程compositor thread将点击事件导航到bar.com
的渲染主线程main thread,并安排一个合成器事件循环compositor event loop任务来处理它bar.com
的主线程的输入事件探测input event hit testing通过测试来确定iframe中的哪个DOM元素被点击,并「唤起」一个点击事件供脚本观察。「分享是一种态度」,这篇文章,是一篇译文,算是一个自我学习过程中的一种记录和总结。主要是把自己认为重要的点,都罗列出来。同时,也是为大家节省一下「排雷和踩坑的时间」。当然,可能由于自己认知能力所限,有些点,没能表达很好。
参考资料: