谷歌官方博客:深入了解现代浏览器系列(其三)

1,652 阅读10分钟

给前端以福利,给编程以复利。大家好,我是大家的林语冰。

00. 写在前面

本系列的前面两篇博客中,我们科普了多进程架构和导航流程。这是探讨浏览器工作原理的博客系列第 3 部分。在本文中,我们将探讨渲染器进程内部的技术细节。

渲染器进程涉及 Web 性能的方方面面。由于渲染器进程内部发生了很多事情,因此本文只是一个宏观视角。

免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 Inside look at modern web browser (part 3)

01. 渲染器进程处理网页内容

渲染器进程负责选项卡内发生的所有事情。在渲染器进程中,主线程处理发送给用户的大部分代码。如果你使用了 Web Worker 或 Service Worker,有时 JavaScript 的部分工作是由 worker 线程处理的。

合成器和光栅线程也在渲染器进程内运行,高效丝滑地渲染页面。渲染器进程的核心职责是将 HTML、CSS 和 JavaScript 转换为用户可以交互的网页。

01-render.png

02. 解析

02-1. 构建 DOM

当渲染器进程收到导航的提交消息,并开始接收 HTML 数据时,主线程开始解析文本字符串(HTML),并将其转换为 DOM(文档对象模型)。

DOM 是浏览器页面的内部表示,也是 Web 开发者可以通过 JavaScript 进行交互的数据结构和 API。

将 HTML 文档解析为 DOM 是由 HTML 标准定义的。向浏览器提供 HTML 永远不会引发错误。

举个栗子,缺少 </p> 闭合标签也是有效的 HTML。诸如 Hi! <b>I'm <i>Chrome</b>!</i> 之类的错误标签将被解读为 Hi! <b>I'm <i>Chrome</i></b><i>!</i> 。这是因为 HTML 规范旨在优雅地处理这些错误。

02-2. 子资源加载

网站通常使用图像、CSS 和 JavaScript 等外部资源。这些文件需要从网络或缓存加载。主线程可以在解析构建 DOM 时一一请求它们,但为了加快速度,“预加载扫描器”是并发运行的。

如果 HTML 文档中存在诸如 <img> 或 <link> 之类的内容,那么预加载扫描器会查看 HTML 解析器生成的标签,并将请求发送到浏览器进程中的网络线程。

02-sub.png

02-3. JavaScript 可能阻塞解析

当 HTML 解析器找到 <script> 标签时,它会暂停 HTML 文档的解析,并必须加载、解析和执行 JavaScript 代码。

为什么呢?因为 JavaScript 可以使用诸如 document.write() 之类的”黑魔法“,改变文档的形状,这会改变整个 DOM 结构。这就是为什么 HTML 解析器必须等待 JavaScript 运行,才能恢复 HTML 文档的解析。

03. 向浏览器明示你要如何加载资源

Web 开发者可以通过多种方式向浏览器发送提示,更好地加载资源。

如果你的 JavaScript 不使用 document.write() ,你可以将 async 或 defer 属性添加到 <script> 标签中。然后浏览器异步加载并运行 JavaScript 代码,并且不会阻塞解析。

如果条件合适的话,你也可以使用 JavaScript 模块。<link rel="preload"> 也是一种通知浏览器当前导航肯定需要该资源、并且你希望尽快下载的方式。

04. 样式计算

拥有 DOM 不足以了解页面的外观,因为我们可以在 CSS 中设置页面元素的样式。

主线程会解析 CSS,并确定每个 DOM 节点的计算样式。这是有关基于 CSS 选择器对每个元素应用哪种样式的信息。

03-style.png

即使你不提供任何 CSS,每个 DOM 节点也有一个计算样式。

举个栗子,<h1> 标签显示得比 <h2> 标签大,并且为每个元素定义了边距。这是因为浏览器就有一个默认的样式表。

05. 布局

现在渲染器进程知道文档的结构和每个节点的样式,但这还不足以渲染页面。

请想象一下,你正试图和女朋友微信语音,并向她描述一幅画。 “有一个红色的大圆圈,和一个蓝色的小方块”,这并不足以让你的女朋友知道这幅画到底是什么样子。

04-call.png

布局是寻找元素几何形状的过程。主线程遍历 DOM 和计算样式并创建布局树,其中包含 x y 坐标和边界框大小等信息。

布局树可能与 DOM 树的结构类似,但它只包含与页面上可见内容相关的信息。如果应用 display: none,那么该元素就不是布局树的一部分。但是,具有 visibility: hidden 的元素位于布局树中。

举一反一,如果应用了具有诸如 p::before{content:"Hi!"} 之类的伪类,它也会包含在布局树中,即使它不在 DOM 中。

05-tree.png

06-layout.gif

确定页面的布局极具挑战。即使是最简单的页面布局,比如从上到下的区块流,也必须考虑字体有多大,以及在哪里换行,因为这些会影响段落的大小和形状;这也会影响下一段的位置。

CSS 可以使元素单侧浮动、屏蔽溢出,以及改变书写方向。可以想象,这个布局阶段的任务无比艰巨。

06. 绘制

07-paint.png

拥有 DOM、样式和布局仍然不足以渲染页面。

假设你正在尝试拷贝一幅画。你知道元素的大小、形状和位置,但你仍然必须判断绘制它们的顺序。

举个栗子,你可能会为某些元素设置 z-index,在这种情况下,按照 HTML 中编写的元素顺序绘制将导致错误渲染。

08-index.png

在绘制步骤中,主线程遍历布局树,创建绘制记录。

绘制记录是对绘制过程的记录,“先背景,后文字,再矩形”。如果你使用过 JavaScript 绘制过 <canvas> 元素,那么你可能会熟悉此过程。

09-record.png

更新渲染管道的成本很高。

10-cost.gif

在渲染管道中最重要的一点是,在每一步中,都会使用先前操作的结果来创建新数据。

举个栗子,如果布局树中发生某些变化,那么需要为文档的受影响部分重新生成绘制顺序。

如果你要对元素进行动画处理,那浏览器必须在每一帧之间运行这些操作。我们的大多数显示器每秒刷新屏幕 60 次;当你在屏幕上每帧移动物体时,动画对于人眼来说会显得平滑。

但是,如果动画错过了中间的帧,那么页面将出现“卡顿现象”。

11-jank.png

即使你的渲染操作与屏幕刷新保持同步,这些计算也会在主线程上运行,这意味着,当你的应用程序运行 JavaScript 时,它可能会被阻塞。

12-frame.png

你可以将 JavaScript 操作分成小块,并使用 requestAnimationFrame() 安排在每一帧运行。你还可以在 Web Workers 中运行 JavaScript,避免阻塞主线程。

13-raq.png

07. 合成

07-1. 如何绘制页面?

14-view.gif

既然浏览器知道了文档的结构、每个元素的样式、页面的几何形状以及绘制顺序,那么它如何绘制页面呢?

将这些信息转换成屏幕上的像素称为 光栅化(rasterizing)。

也许处理这个问题的一个简单方案是在视口内对部分进行光栅化。如果用户滚动页面,则移动光栅框架,并通过光栅更多来填充缺失的部分。这就是 Chrome 首发时处理光栅化的方案。

然而,现代浏览器运行一个更复杂的过程,称为 合成(compositing)。

07-2. 什么是合成

15-compose.gif

合成是一种将页面的各个部分分成图层、分别光栅化,并在称为合成器线程的单独线程中合成为页面的技术。

如果发生滚动,由于图层已经光栅化,它所要做的就是合成一个新帧。可以通过移动图层并合成新帧以相同的方式实现动画。

07-3. 分层

为了找出哪些元素需要位于哪些图层中,主线程会遍历布局树来创建图层树,这在 DevTools 性能面板中称为“更新图层树”。

如果页面的某些部分应该是单独的图层,比如滑入式侧面菜单,但没有获得单独的图层,那么你可以使用 CSS 中的 will-change 属性向浏览器提示。

16-layer.png

你可能会想为每个元素提供图层,但是与每帧光栅化页面的一小部分相比,跨过多数量的图层进行合成可能会导致操作速度变慢,因此衡量应用程序的渲染性能至关重要。

07-3. 主线程的光栅和合成

一旦创建了图层树并确定了绘制顺序,主线程就会将该信息提交给合成器线程。然后合成器线程光栅化每一层。

图层可能像页面的整个长度一样大,因此合成器线程将它们划分为图块,并将每个图块发送到光栅线程。光栅线程对每个图块进行光栅化,并将其存储在 GPU 内存中。

17-raser.png

合成器线程可以优先考虑不同的光栅线程,以便可以首先对视口内或附近的事物进行光栅化。

图层还具有针对不同分辨率的多个平铺,以处理诸如放大操作之类的操作。一旦图块被光栅化,合成器线程就会收集称为绘制四边形的图块信息,来创建合成器框架。

然后,合成器框架通过 IPC 提交给浏览器进程。此时,可以从 UI 线程添加另一个合成器框架,进行浏览器 UI 更改,或者从其他渲染器进程添加进行扩展。

这些合成器帧被发送到 GPU,将其显示在屏幕上。如果出现滚动事件,合成器线程将创建另一个合成器帧发送到 GPU。

18-gpu.png

合成的优势在于,它是在不涉及主线程的情况下完成的。合成器线程不需要等待样式计算或 JavaScript 执行。

这就是为什么仅合成动画被认为是获得流畅性能的最佳选择。如果需要重新计算布局或绘制,那必然涉及主线程。

高潮总结

在这篇文章中,我们深度学习了从解析到合成的渲染管道。

在本系列的最后一篇博客中,我们将更详细地了解合成器线程,并了解当 mouse move 和 click 等用户输入进入时,浏览器幕后的技术细节。

参考文献

粉丝互动

本期话题是:浏览器如何构建渲染树和绘制页面的?你可以在本文下方自由言论,文明科普。

欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。

坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~

26-cat.gif