Jason Pan

潘忠显 / 2021-04-11


“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显

How JavaScript works: the rendering engine and tips to optimize its performance

JavaScript的工作方式:渲染引擎和优化其性能的技巧

这是该系列的第11个帖子,专门探讨JavaScript及其构建组件。在标识和描述核心元素的过程中,我们还共享一些在构建[SessionStack]时使用的经验法则。(https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=js-series-rendering-engine -intro),这是一个JavaScript应用程序,需要强大且高效能,以帮助用户实时查看和再现其Web应用程序缺陷。

This is post # 11 of the series dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript application that needs to be robust and highly-performant to help users see and reproduce their web app defects real-time.

如果错过了前几章,可以在这里找到它们:

If you missed the previous chapters, you can find them here:

到目前为止,在我们之前的“ JavaScript的工作原理”系列博客文章中,我们一直专注于JavaScript作为一种语言,其功能,如何在浏览器中执行,如何对其进行优化等。

但是,在构建Web应用程序时,您不仅可以编写独立运行的独立JavaScript代码。您编写的JavaScript正在与环境交互。了解此环境,它的工作方式以及它的组成将使您能够构建更好的应用程序,并为一旦应用程序发布后可能出现的潜在问题做好充分的准备。

So far, in our previous blog posts of the “How JavaScript works” series we’ve been focusing on JavaScript as a language, its features, how it gets executed in the browser, how to optimize it, etc.

When you’re building web apps, however, you don’t just write isolated JavaScript code that runs on its own. The JavaScript you write is interacting with the environment. Understanding this environment, how it works and what it is composed of will allow you to build better apps and be well-prepared for potential issues that might arise once your apps are released into the wild.

img

因此,让我们看看浏览器的主要组成部分是:

-用户界面:包括地址栏,后退和前进按钮,书签菜单等。实质上,这是浏览器显示的每个部分,但您可以看到网页本身的窗口除外。 -浏览器引擎:它处理用户界面和渲染引擎之间的交互 -渲染引擎:它负责显示网页。呈现引擎解析HTML和CSS,并在屏幕上显示解析的内容。 -网络:这些是诸如XHR请求之类的网络调用,它们是针对不同平台使用不同的实现而产生的,它们位于独立于平台的界面后面。我们在本系列的[上一篇文章]()中更详细地讨论了网络层。 -** UI后端**:用于绘制核心小部件,例如复选框和窗口。该后端公开了不是平台特定的通用接口。它在下面使用操作系统UI方法。 -** JavaScript引擎**:我们在本系列的[上一篇文章]()中对此进行了详细介绍。基本上,这就是执行JavaScript的地方。 -**数据持久性**:您的应用可能需要在本地存储所有数据。支持的存储机制类型包括[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage),[indexDB](https://developer.mozilla.org/ zh-CN / docs / Web / API / IndexedDB_API),[WebSQL](https://en.wikipedia.org/wiki/Web_SQL_Database)和[FileSystem](https://developer.mozilla.org/zh-CN/ docs / Web / API / FileSystem)。

So, let’s see what the browser main components are:

在本文中,我们将重点介绍渲染引擎,因为它处理HTML和CSS的解析和可视化,而大多数JavaScript应用程序一直在与之交互。

In this post, we’re going to focus on the rendering engine, since it’s handling the parsing and the visualization of the HTML and the CSS, which is something that most JavaScript apps are constantly interacting with.

Overview of the rendering engine

##渲染引擎概述

呈现引擎的主要职责是在浏览器屏幕上显示请求的页面。

渲染引擎可以显示HTML和XML文档和图像。如果您使用的是其他插件,则引擎还可以显示不同类型的文档,例如PDF。

The main responsibility of the rendering engine is to display the requested page on the browser screen.

Rendering engines can display HTML and XML documents and images. If you’re using additional plugins, the engines can also display different types of documents such as PDF.

Rendering engines

##渲染引擎

与JavaScript引擎类似,不同的浏览器也使用不同的呈现引擎。这些是一些受欢迎的:

-** Gecko **-Firefox -** WebKit **-Safari -**闪烁**-Chrome,Opera(从版本15开始)

Similar to the JavaScript engines, different browsers use different rendering engines as well. These are some of the popular ones:

The process of rendering

##渲染过程

呈现引擎从网络层接收所请求文档的内容。

The rendering engine receives the contents of the requested document from the networking layer.

img

Constructing the DOM tree

##构造DOM树

呈现引擎的第一步是解析HTML文档,并将解析的元素转换为实际的[DOM](https://developer.mozilla.org/zh-CN/docs/Web/API/Document_Object_Model/Introduction)节点。 ** DOM树**。

假设您有以下文字输入:

The first step of the rendering engine is parsing the HTML document and converting the parsed elements to actual DOM nodes in a DOM tree.

Imagine you have the following textual input:

<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树如下所示:

The DOM tree for this HTML will look like this:

img

基本上,每个元素都表示为所有元素的父节点,这些元素直接包含在其中。这是递归应用的。

Basically, each element is represented as the parent node to all of the elements, which are directly contained inside of it. And this is applied recursively.

Constructing the CSSOM tree

##构造CSSOM树

CSSOM是指** CSS对象模型**。当浏览器构建页面的DOM时,它在head部分遇到了一个link标记,该标记引用了外部theme.css CSS样式表。预期它可能需要该资源来呈现页面,因此立即为它调度了一个请求。假设theme.css文件具有以下内容:

CSSOM refers to the CSS Object Model. While the browser was constructing the DOM of the page, it encountered a link tag in the head section which was referencing the external theme.css CSS style sheet. Anticipating that it might need that resource to render the page, it immediately dispatched a request for it. Let’s imagine that the theme.css file has the following contents:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

与HTML一样,引擎需要将CSS转换为浏览器可以使用的东西-CSSOM。 CSSOM树如下所示:

As with the HTML, the engine needs to convert the CSS into something that the browser can work with — the CSSOM. Here is how the CSSOM tree will look like:

img

您想知道CSSOM为什么具有树结构吗?在计算页面上任何对象的最终样式集时,浏览器会从适用于该节点的最通用规则开始(例如,如果它是body元素的子代,则所有body样式都适用),然后递归优化通过应用更具体的规则来计算样式。

让我们处理我们给出的具体示例。放置在body元素内的span标签中包含的任何文本的字体大小均为16像素,并且颜色为红色。这些样式是从body元素继承的。如果span元素是p元素的子元素,则由于要应用更具体的样式,因此不会显示其内容。

Do you wonder why does the CSSOM have a tree structure? When computing the final set of styles for any object on the page, the browser starts with the most general rule applicable to that node (for example, if it is a child of a body element, then all body styles apply) and then recursively refines the computed styles by applying more specific rules.

Let’s work with the specific example that we gave. Any text contained within a span tag that is placed within the body element, has a font size of 16 pixels and has a red color. Those styles are inherited from the body element. If a span element is a child of a p element, then its contents are not displayed due to the more specific styles that are being applied to it.

另外,请注意,上面的树不是完整的CSSOM树,仅显示我们决定在样式表中覆盖的样式。每种浏览器都提供一组默认样式,也称为**“用户代理样式” **-当我们未明确提供任何样式时,便会看到这些样式。我们的样式仅覆盖这些默认值。

Also, note that the above tree is not the complete CSSOM tree and only shows the styles we decided to override in our style sheet. Every browser provides a default set of styles also known as “user agent styles” — that’s what we see when we don’t explicitly provide any. Our styles simply override these defaults.

Constructing the render tree

##构造渲染树

HTML中的视觉指令与CSSOM树中的样式数据结合在一起,用于创建渲染树

您可能会问什么是渲染树?这是按照在屏幕上显示顺序排列的可视元素的树。它是HTML以及相应CSS的可视表示形式。该树的目的是使按正确的顺序绘画内容。

The visual instructions in the HTML, combined with the styling data from the CSSOM tree, are being used to create a render tree.

What is a render tree you may ask? This is a tree of the visual elements constructed in the order in which they will be displayed on the screen. It is the visual representation of the HTML along with the corresponding CSS. The purpose of this tree is to enable painting the contents in their correct order.

渲染树中的每个节点在Webkit中称为渲染器或渲染对象。

这就是上面的DOM和CSSOM树的渲染器树的样子:

Each node in the render tree is known as a renderer or a render object in Webkit.

This is how the renderer tree of the above DOM and CSSOM trees will look like:

img

为了构造渲染树,浏览器大致执行以下操作:

-从DOM树的根部开始,它遍历每个可见节点。一些节点不可见(例如,脚本标记,元标记等),由于它们未反映在渲染的输出中,因此将其省略。一些节点通过CSS隐藏,并且在渲染树中也被省略。例如,span节点-在上例中它不存在于渲染树中,因为我们有一个明确的规则在其上设置display:none属性。 -对于每个可见节点,浏览器会找到合适的匹配CSSOM规则并将其应用。 -发出带有内容及其计算样式的可见节点

To construct the render tree, the browser does roughly the following:

您可以在此处查看RenderObject的源代码(在WebKit中):https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

You can take a look at the RenderObject’s source code (in WebKit) here: https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

让我们看一下该课程的一些核心内容:

Let’s just look at some of the core things for this class:

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框相对应的矩形区域。它包括几何信息,例如宽度,高度和位置。

Each renderer represents a rectangular area usually corresponding to a node’s CSS box. It includes geometric info such as width, height, and position.

Layout of the render tree

##渲染树的布局

创建渲染器并将其添加到树时,它没有位置和大小。计算这些值称为布局。

HTML使用基于流的布局模型,这意味着大多数时候它可以一次计算几何。坐标系是相对于根渲染器的。使用左上角坐标。

布局是一个递归过程-它始于根渲染器,它与HTML文档的<html>元素相对应。布局通过部分或整个渲染器层次结构递归地继续,为需要它的每个渲染器计算几何信息。

When the renderer is created and added to the tree, it does not have a position and size. Calculating these values is called layout.

HTML uses a flow-based layout model, meaning that most of the time it can compute the geometry in a single pass. The coordinate system is relative to the root renderer. Top and left coordinates are used.

Layout is a recursive process — it begins at the root renderer, which corresponds to the <html> element of the HTML document. Layout continues recursively through a part or the entire renderer hierarchy, computing geometric info for each renderer that requires it.

根渲染器的位置为“ 0,0”,其尺寸具有浏览器窗口(也称为视口)可见部分的大小。

开始布局过程意味着为每个节点提供应在屏幕上显示的确切坐标。

The position of the root renderer is 0,0 and its dimensions have the size of the visible part of the browser window (a.k.a. the viewport).

Starting the layout process means giving each node the exact coordinates where it should appear on the screen.

Painting the render tree

##绘制渲染树

在此阶段,遍历渲染器树,并调用渲染器的“ paint()”方法以在屏幕上显示内容。

绘画可以是全局的也可以是增量的(类似于布局):

-全局-整个树都被重新粉刷。 -增量**-仅某些渲染器以不影响整个树的方式进行更改。渲染器使屏幕上的矩形无效。这导致操作系统将其视为需要重绘的区域并生成paint事件。操作系统通过将多个区域合并为一个区域,以一种智能的方式来完成此任务。

In this stage, the renderer tree is traversed and the renderer’s paint() method is called to display the content on the screen.

Painting can be global or incremental (similar to layout):

一般而言,了解绘画是一个循序渐进的过程很重要。为了获得更好的UX,渲染引擎将尝试尽快在屏幕上显示内容。它不会等到所有HTML都解析完毕后开始构建并布局渲染树。内容的一部分将被解析和显示,而该过程将继续处理其余的内容项,这些内容项始终来自网络。

In general, it’s important to understand that painting is a gradual process. For better UX, the rendering engine will try to display the contents on the screen as soon as possible. It will not wait until all the HTML is parsed to start building and laying out the render tree. Parts of the content will be parsed and displayed, while the process continues with the rest of the content items that keep coming from the network.

Order of processing scripts and style sheets

##处理脚本和样式表的顺序

当解析器到达一个