本文的目的是用非常简单的术语解释浏览器如何将 HTML、CSS 和 JavaScript 转换为我们可以与之交互的网站。了解这个过程,可以帮助你优化 Web 应用程序,从而获得更快的速度和更好的性能。
浏览器如何渲染网站?很快我就会解构这个过程,但是首先,有必要回顾一些基础知识。
Web 浏览器是一种软件,它从远程服务器(或者本地磁盘)加载文件并将其显示给你——使用户可以与之交互。
我知道你知道浏览器是什么。不过,在浏览器中有一个软件叫浏览器引擎。在不同的浏览器中,浏览器的某个部分会根据它接收到的文件确定显示什么,这就是所谓的浏览器引擎。浏览器引擎是每一种主流浏览器的核心软件组件,不同的浏览器开发商用不同的名字来称呼他们的引擎。Firefox 的浏览器引擎叫做 Gecko,Chrome 的引擎叫做 Blink,而 Blink 恰好是 Webkit 的一个分支。别被这些名字弄糊涂了。它们只是名字。没有什么高深的。
为了演示,假设我们有一个通用的浏览器引擎,如下所示:
如果你感兴趣的话,可以看看各种浏览器引擎的比较。
在本文中,我会交替使用“浏览器”和“浏览器引擎”,别被这弄糊涂了。重要的是,你要知道,对于我们正在讨论的内容,浏览器引擎是关键软件。
这不是计算机科学网络的课程,但是你可能得记住,数据是以“数据包”的形式通过互联网发送,而数据包以字节为单位。
我想说的是,当你编写一些 HTML、CSS 和 JS,并试图在浏览器中打开 HTML 文件时,浏览器会从你的硬盘(或网络)中读取 HTML 的原始字节。
明白了吗?浏览器读取的是原始数据字节,而不是你编写的代码的实际字符。
让我们继续。浏览器接收字节数据,但是,它用这些数据什么都做不了。数据的原始字节必须转换为它所理解的形式,这是第一步。
浏览器对象需要处理的是文档对象模型(DOM)对象。那么,DOM 对象是从何而来的呢?这很简单。首先,将原始数据字节转换为字符。
这一点,你可以通过你所编写的代码的字符看到。这种转换是基于 HTML 文件的字符编码完成的。至此,浏览器已经从原始数据字节转换为文件中的实际字符。但这不是最终的结果。这些字符会被进一步解析为一些称为“标记(token)”的东西。
那么,这些标记是什么?文本文件中的一堆字符对浏览器引擎而言没什么用处。如果没有这个标记化过程,那么这一堆堆字符只会生成一系列毫无意义的文本,即 HTML 代码——不会生成一个真正的网站。
当你保存一个扩展名为.html 的文件时,就向浏览器引擎发出了把文件解析为 HTML 文档的信号。浏览器“解释”这个文件的方式是首先解析它。在解析过程中,特别是在标记化过程中,浏览器会解析 HTML 文件中的每个开始和结束“标签(tag)”。解析器可以识别尖括号中的每个字符串,如“< html>”、“< p>”,也可以推断出适用于其中任何一个字符串的规则集。例如,表示锚标签的标记与表示段落标签的标记具有不同的属性。
从概念上讲,你可以将标记看作某种数据结构,它包含关于某个 HTML 标签的信息。本质上,HTML 文件会被分解成称为标记的小的解析单元。浏览器就是这样开始识别你所编写的内容的。
但标记还不是最终的结果。标记化完成后,接下来,标记将被转换为节点。你可以将节点看作是具有特定属性的不同对象。实际上,更好的解释是,将节点看作是文档对象树中的独立实体。但节点仍然不是最终结果。
现在,让我们看一下最后一点。在创建好之后,这些节点将被链接到称为 DOM 的树数据结构中。DOM 建立起了父子关系、相邻兄弟关系等。在这个 DOM 对象中,每个节点之间都建立了关系。现在,这是我们可以使用的东西了。
如果你还记得 Web design 101,你不会在浏览器中打开 CSS 或 JS 文件来查看网页。你打开的是 HTML 文件,大多数情况下是 index.html 这种方式。
你之所以会这样做,是因为浏览器会首先将 HTML 数据的原始字节转换为 DOM。
根据 HTML 文件的大小,DOM 的构建过程可能需要一些时间。无论文件多小,它都需要一些时间。
DOM 已经创建。带有一些 CSS 的典型 HTML 文件会包含下面这样的样式表链接:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>
</body>
</html>
当浏览器接收到原始数据字节并启动 DOM 构建过程时,它还会发出请求来获取链接的 main.css 样式表。当浏览器开始解析 HTML 时,在找到 css 文件的链接标签的同时,它会发出请求来获取它。可能你已经猜到,浏览器还是接收 CSS 数据的原始字节,从互联网或是本地磁盘。
但是,浏览器如何处理这些 CSS 数据的原始字节呢?
当浏览器接收到 CSS 的原始字节时,会启动一个和处理 HTML 原始字节类似的过程。就是说,原始数据字节被转换成字符,然后标记,然后形成节点,最后形成树结构。
什么是树结构?大多数人都知道 DOM 这个词。同样,也有一种 CSS 树结构,称为 CSS 对象模型,简称为 CSSOM。
你知道,浏览器不能使用 HTML 或 CSS 的原始字节。必须将其转换成它能识别的形式,也就是这些树形结构。
CSS 有一个叫做级联的东西。级联是浏览器确定如何在元素上应用样式的机制。
由于影响元素的样式可能来自父元素,即通过继承,或者已经在元素本身设置,所以 CSSOM 树结构变得很重要。为什么?这是因为浏览器必须递归遍历 CSS 树结构并确定影响特定元素的样式。
一切顺利。浏览器有了 DOM 和 CSSOM 对象。现在,我们能在屏幕上呈现一些东西了吗?
我们现在得到的是两个独立的树结构,它们似乎没有共同的目标。
DOM 和 CSSOM 树结构是两个独立的树结构。DOM 中包含关于页面 HTML 元素关系的所有信息,而 CSSOM 则包含关于元素样式的信息。
好了,浏览器现在把 DOM 和 CSSOM 树组合成一棵渲染树。
渲染树包含页面上所有关于可见 DOM 内容的信息以及不同节点所需的所有 CSSOM 信息。注意,如果一个元素被 CSS 隐藏,例如使用 display; none,那么节点就不会包含在渲染树中。隐藏元素会出现在 DOM 中,但不会出现在渲染树中。这是因为渲染树结合了来自 DOM 和 CSSOM 的信息,所以它知道不能把隐藏元素包含在树中。
构建好渲染树之后,浏览器将继续下个步骤:布局!
现在,我们有了屏幕上的内容和所有可见内容的样式信息——但是我们并没有实际在屏幕上渲染任何内容。
首先,浏览器必须计算页面上每个对象的确切大小和位置。这就好比是,把要在页面上渲染的所有元素的内容和样式信息传递给一个有才华的数学家。然后,这位数学家用浏览器的视窗计算出每个元素的确切位置和大小。
这个布局步骤考虑了从 DOM 和 CSSOM 接收到的内容和样式,并执行了所有必要的布局计算。有时,你会听到人们把这个“布局”阶段称为“回流(reflow)”。
现在,每个元素的确切位置已经计算出来,剩下的就是将元素“绘制”到屏幕上。
考虑一下。我们已经得到了在屏幕上显示元素所需的所有信息。我们只要把它展示给用户。这就是这个阶段的全部工作。有了元素内容(DOM)、样式(CSSOM)和计算得出的元素的精确布局信息,浏览器现在就可以将节点逐个“绘制”到屏幕上了。元素现在终于呈现在屏幕上了!
当你听到“渲染阻塞(render-blocking)”时,你会想到什么?我猜你想的是,“有东西阻止了屏幕上节点的实际绘制”。如果你这么说,那你说的完全正确!
优化网站的第一准则是让最重要的 HTML 和 CSS 尽可能快地传递到客户端。在成功绘制之前,必须构造 DOM 和 CSSOM,因此,HTML 和 CSS 都是渲染阻塞资源。关键是,你应该尽快将 HTML 和 CSS 提供给客户端,以优化应用程序的首次渲染时间。
一个好的 Web 应用程序肯定会使用一些 JavaScript。这是一定的。JavaScript 的“问题”在于你可以使用 JavaScript 修改页面的内容和样式。通过这种方式,你可以从 DOM 树中删除元素和添加元素,还可以通过 JavaScript 修改元素的 CSSOM 属性。
这很棒!但是要付出代价的。考虑下面 HTML 文档:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
这是一个非常简单的文档。样式表 style.css 只有下面一个声明语句:
body {
background: #8cacea;
}
一段简单的文本和图像呈现在屏幕上。
根据前面的解释,浏览器从磁盘(或网络)读取 HTML 文件的原始字节并将其转换为字符。字符被进一步解析为标记。当解析器遇到< link rel="stylesheet" href="style.css">时,就会请求获取 CSS 文件 style.css。DOM 构造继续进行,当 CSS 文件返回一些内容后,CSSOM 构造就开始了。
引入 JavaScript 后,这个过程会发生什么变化?要记住,其中最重要的一件事情是,每当浏览器遇到脚本标签时,DOM 构造就会暂停!整个 DOM 构建过程都将停止,直到脚本执行完成。
这是因为 JavaScript 可以同时修改 DOM 和 CSSOM。由于浏览器不确定特定的 JavaScript 会做什么,所以它采取的预防措施是停止整个 DOM 构造。
让我们看一下,这有多么糟糕。在之前分享的基本 HTML 文档中,我们引入一个脚本标签,其中包含一些基本的 JavaScript 语句:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
<script>
let header = document.getElementById("header");
console.log("header is: ", header);
</script>
</body>
</html>
在脚本标签中,我将访问 id 为 header 的 DOM 节点,然后将其输出到控制台。可以正常运行,如下所示:
但是,你是否注意到,这个脚本标签位于 body 标签的底部?让我们把它放在 head 中,看看会发生什么:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
<script>
let header = document.getElementById("header");
console.log("header is: ", header);
</script>
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
一旦我这样做,header 就解析为 null。
为什么会这样?很简单。当 HTML 解析器正在构建 DOM 时,发现了一个脚本标签。此时,body 标签及其所有内容还没有被解析。DOM 构造将停止,直到脚本执行完成:
当脚本试图访问一个 id 为 header 的 DOM 节点时,由于 DOM 还没有完成对文档的解析,所以它还不存在。这把我们带到了另一个重要的问题。脚本的位置很重要。
这还不是全部。如果你将内联脚本提取到外部本地文件 app.js 中,行为是一样的。DOM 的构建仍然会停止:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
那么,如果 app.js 不是本地的而必须通过互联网获取呢?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
<script src="https://some-link-to-app.js"></script>
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
如果网速很慢,需要数千毫秒来获取 app.js,DOM 的构建也会暂停几千毫秒!!!这是一个很大的性能问题,而且还不止于此。JavaScript 还可以访问 CSSOM 并对其进行修改。例如,这是有效的 JavaScript 语句:
document.getElementsByTagName("body")[0].style.backgroundColor = "red";
那么,当解析器遇到一个脚本标签而 CSSOM 还没有准备好时,会发生什么情况呢?答案很简单。Javascript 执行将会停止,直到 CSSOM 就绪。
因此,虽然 DOM 构造在遇到脚本标签时会停止,但 CSSOM 不会发生这种情况。对于 CSSOM,JS 执行会等待。没有 CSSOM,就没有 JS 执行。
在默认情况下,每个脚本都是一个解析器阻断器!DOM 的构建总是会被打断。不过,有一种方法可以改变这种默认行为。如果将 async 关键字添加到脚本标签中,那么 DOM 构造就不会停止。DOM 构造将继续,脚本将在下载完成并准备就绪后执行。
这里有一个例子:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
<script src="https://some-link-to-app.js" async></script>
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
目前为止,我们讨论了从接收 HTML、CSS 和 JS 字节到将它们转换为屏幕上的像素之间的所有步骤。这整个过程称为关键渲染路径。优化网站性能就是优化关键渲染路径。
一个经过良好优化的站点应该能够渐进式渲染,而不是让整个过程受阻。
这是 Web 应用程序慢或快的区别所在。周密的关键渲染路径(CRP)优化策略使浏览器能够通过确定优先加载的资源以及资源加载的顺序来尽可能快地加载页面。
了解了浏览器如何渲染 HTML、CSS 和 JS 的基础知识后,我建议你花时间研究下如何利用这些知识优化页面,提高速度。
谷歌 Web 基础文档中有关性能的章节 (https://developers.google.com/web/fundamentals/performance/why-performance-matters/) 是一个不错的起点。
https://blog.logrocket.com/how-browser-rendering-works-behind-the-scenes-6782b0e8fb10
邱岳,江湖人称“二爷”,技术人出身的产品经理,7 个月把「抽奖助手」用户从 0 增长到 2000 万。他将在「邱岳的产品手记」专栏中,用 45 堂课分享他在产品领域 10 年的经验心得,帮你培养产品思维,提升你的产品认知。
限时福利:
限时 24 小时拼团仅需¥68,立省¥31。
扫码可免费试读或订阅