翻译自:How JavaScript works: the rendering engine and tips to optimize its performance
这是探索 JavaScript 及其构建组件专题系列的第 11 篇。在识别和描述核心元素的过程中,我们分享了在构建 SessionStack 时使用的一些经验法则。SessionStack 是一个需要鲁棒且高性能的 JavaScript 应用程序,它帮助用户实时查看和重现它们 Web 应用程序的缺陷。
当构建 Web 应用程序时,你不只是编写独立运行的 JavaScript 代码片段。你编写的 JavaScript 需要与环境进行交互。理解环境是如何工作的以及它是由什么组成的,你就能够构建更好的应用程序,并且能更好地处理应用程序发布后才会显现的潜在问题。
那么,让我们看看浏览器的主要组件有哪些:
在这篇文章中,我们将关注渲染引擎,因为它负责处理 HTML 和 CSS 的解析和可视化,这是大多数 JavaScript 应用程序不断与之交互的地方。
渲染引擎的主要职责是在浏览器屏幕上显示所请求的页面。
渲染引擎可以显示 HTML / XML 文档和图像。如果你使用其他插件,它还可以显示不同类型的文档,例如 PDF。
与 JavaScript 引擎类似,不同的浏览器也使用不同的渲染引擎。常见的有这些:
渲染引擎从网络层接收所请求文档的内容。
渲染引擎的第一步是解析 HTML 文档并将解析出的元素转换为 DOM 树 中实际的 DOM 节点。
假设你有以下文字输入:
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="theme.css">
</head>
<body>
<p> Hello, <span> friend! </span> </p>
<div>
<img src="smiley.gif" alt="Smiley face" height="42" width="42">
</div>
</body>
</html>
这个 HTML 的 DOM 树如下所示:
基本上,每个元素都作为它所包含元素的父节点,这个结构是递归的。
CSSOM 指 CSS 对象模型。当浏览器构建页面的 DOM 时,它在 head
中遇到了一个引用外部 theme.css
CSS 样式表的 link
标签。浏览器预计到它可能需要该资源来呈现页面,所以它立即发出请求。让我们假设 theme.css
文件包含以下内容:
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
与 HTML 一样,引擎需要将 CSS 转换为浏览器可以使用的东西 —— CSSOM。以下是 CSSOM 树的样子:
你知道为什么 CSSOM 是树型结构吗?当计算页面上对象的最终样式集时,浏览器以适用于该节点的最一般规则开始(例如,如果它是 body 元素的子元素,则应用 body 的所有样式),然后递归地细化,通过应用更具体的规则来计算样式。
让我们来看看具体的例子。包含在 body
元素内的 span
标签中的任何文本的字体大小均为 16 像素,并且为红色。这些样式是从 body
元素继承而来的。 如果一个 span
元素是一个 p
元素的子元素,那么它的内容就不会被显示,因为它被应用了更具体的样式(display: none
)。
另外请注意,上面的树不是完整的 CSSOM 树,只显示了我们决定在样式表中重写的样式。每个浏览器都提供了一组默认的样式,也称为 「用户代理样式」——这是我们在未明确指定任何样式时看到的样式。我们的样式会覆盖这些默认值。
HTML 中的视图指令与 CSSOM 树中的样式数据结合在一起用来创建渲染树。
你可能会问什么是渲染树。渲染树是一颗由可视化元素以它们在屏幕上显示的顺序而构成的树型结构。它是 HTML 和相应的 CSS 的可视化表示。此树的目的是为了以正确的顺序绘制内容。
渲染树中的节点被称为 Webkit 中的渲染器或渲染对象。
这就是上述 DOM 和 CSSOM 树的渲染器树的样子:
为了构建渲染树,浏览器大致做了如下工作:
display: none
属性。
你可以在这里查看 RenderObject 的源代码(在 WebKit 中):https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h
我们来看看这个类的一些核心内容:
class RenderObject : public CachedImageClient {
// Repaint the entire object. Called when, e.g., the color of a border changes, or when a border
// style changes.
Node* node() const { ... }
RenderStyle* style; // the computed style
const RenderStyle& style() const;
...
}
每个渲染器代表一个矩形区域,通常对应于一个节点的 CSS 盒模型。它包含几何信息,例如宽度、高度和位置。
当渲染器被创建并添加到树中时,它并没有位置和大小。计算这些值的过程称为布局。
HTML 使用基于流的布局模型,这意味着大部分时间内它可以在一次遍历中(single pass)计算出布局。坐标系是相对于根渲染器的,使用左上原点坐标。
布局是一个递归过程 —— 它从根渲染器开始,对应于 HTML 文档的 <html>
元素,通过部分或整个渲染器的层次结构递归地为每个需要布局的渲染器计算布局信息。
根渲染器的位置是 0,0
,并且其尺寸为浏览器窗口(也称为视口)的可见部分的尺寸。
开始布局过程意味着给出每个节点它应该出现在屏幕上的确切坐标。
在这个阶段,浏览器遍历渲染器树,调用渲染器的 paint()
方法在屏幕上显示内容。
绘图可以是全局的或增量式的(与布局类似):
paint
事件的区域。操作系统通过将几个区域合并为一个区域的智能方式来完成绘图。
一般来说,了解绘图是一个渐进的过程是很重要的。为了更好的用户体验,渲染引擎会尝试尽快在屏幕上显示内容。它不会等到所有的 HTML 被分析完毕才开始构建和布置渲染树。一小部分内容先被解析并显示,同时一边从网络获取剩下的内容一边渐进地渲染。
当解析器到达 <script>
标签时,脚本将被立即解析并执行。文档解析将会被暂停,直到脚本执行完毕。这意味着该过程是同步的。
如果脚本是外部的,那么它首先必须从网络获取(也是同步的)。所有解析都会停止,直到网络请求完成。
HTML5 添加了一个选项,可以将脚本标记为异步,此时脚本被其他线程解析和执行。
如果你想优化你的应用,那么你需要关注五个主要方面。这些是您可以控制的地方:
<body>
的宽度会影响子元素的宽度等等。这一切都意味着布局过程是计算密集型的。该绘图是在多个图层完成的。
JavaScript 经常触发浏览器中的视觉变化,构建 SPA 时更是如此。
以下是关于可以优化 JavaScript 哪些部分来改善渲染性能的一些小提示:
setTimeout
或 setInterval
进行视图更新。这些将在帧中某个不确定的时间点上调用 callback
,可能在最后。我们想要做的是在帧开始时触发视觉变化而不是错过它。requestAnimationFrame
、setTimeout
或 setInterval
中运行它们。
通过添加和删除元素、更改属性等来修改 DOM 会导致浏览器重新计算元素样式,并且在很多情况下还会重新布局整个页面或至少其中的一部分。
要优化渲染性能,请考虑以下方法:
布局的重新计算会对浏览器造成很大压力。请考虑下面的优化:
flexbox
而不是老的布局模型。它运行速度更快,可为你的应用程序创造巨大的性能优势。box.offsetHeight
是没问题的。 但是,如果你在查询元素之前更改了元素的样式(例如,动态向元素添加一些 CSS 类),浏览器必须先应用样式更改并执行布局过程。这可能非常耗时且耗费资源,因此请尽可能避免。
优化绘图
这通常是所有任务中运行时间最长的,因此尽可能避免这种情况非常重要。 以下是我们可以做的事情:
渲染是 SessionStack 运行的重点之一。当用户浏览你的 web 应用遇到问题时,SessionStack 必须将这些遇到的问题重建成一个视频。为了做到这点,SessionStack 仅利用我们的库收集到数据:用户事件、DOM 更改、网络请求、异常和调试消息等。我们的播放器经过高度优化,能够按顺序正确呈现和使用所有收集到的数据,从视觉和技术两方面为你提供用户在浏览器中发生的一切的像素级完美模拟。
如果你想试试看,这里可以免费尝试 SessionStack。
本文来自网易实践者社区,经作者徐子航授权发布。