JavaScript是如何工做的:渲染引擎和优化其性能的技巧

阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。javascript

这是专门探索 JavaScript 及其所构建的组件的系列文章的第11篇。css

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!html

若是你错过了前面的章节,能够在这里找到它们:前端

  1. JavaScript 是如何工做的:引擎,运行时和调用堆栈的概述!
  2. JavaScript 是如何工做的:深刻V8引擎&编写优化代码的5个技巧!
  3. JavaScript 是如何工做的:内存管理+如何处理4个常见的内存泄漏 !
  4. JavaScript 是如何工做的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
  5. JavaScript 是如何工做的:深刻探索 websocket 和HTTP/2与SSE +如何选择正确的路径!
  6. JavaScript 是如何工做的:与 WebAssembly比较 及其使用场景 !
  7. JavaScript 是如何工做的:Web Workers的构建块+ 5个使用他们的场景!
  8. JavaScript 是如何工做的:Service Worker 的生命周期及使用场景!
  9. JavaScript 是如何工做的:Web 推送通知的机制!
  10. JavaScript是如何工做的:使用 MutationObserver 跟踪 DOM 的变化

当你构建 Web 应用程序时,你不仅是编写单独运行的 JavaScript 代码,你编写的 JavaScript 正在与环境进行交互。了解这种环境,它的工做原理以及它的组,这些有助于你够构建更好的应用程序,并为应用程序发布后可能出现的潜在问题作好充分准备。java

图片描述

浏览器的主要组件包括:git

  • 用户界面 (User interface): 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口以外的其余部分
  • 浏览器引擎 (Browser engine):用来查询及操做渲染引擎的接口
  • 渲染引擎 (Rendering engine):用来显示请求的内容,例如,若是请求内容为 html,它负责解析 html 及 css,并将解析后的结果显示出来
  • 网络 (Networking):用来完成网络调用,例如http请求,它具备平台无关的接口,能够在不一样平台上工做
  • UI 后端 (UI backend):用来绘制相似组合选择框及对话框等基本组件,具备不特定于某个平台的通用接口,底层使用操做系统的用户接口
  • JS 解释器 (JavaScript engine):用来解释执行JS代码
  • 数据存储 (Data persistence): 属于持久层,浏览器须要在硬盘中保存相似 cookie 的各类数据,HTML5定义了 Web Database 技术,这是一种轻量级完整的客户端存储技术,支持的存储机制类型包括 localStorageindexDBWebSQLFileSystem

在这篇文章中,将重点讨论渲染引擎,由于它处理 HTML 和 CSS 的解析和可视化,这是大多数 JavaScript 应用程序常常与之交互的东西。github

渲染引擎概述

渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。web

渲染引擎能够显示 HTML 和 XML 文档和图像。若是使用其余插件,渲染引擎还能够显示不一样类型的文档,如 PDF。编程

渲染引擎 (Rendering engines)

与 JavaScript 引擎相似,不一样的浏览器也使用不一样的渲染引擎。如下是一些最受欢迎的:segmentfault

  • Gecko — Firefox
  • WebKit — Safari
  • Blink — Chrome,Opera (版本 15 以后)

Firefox、Chrome 和 Safari 是基于两种渲染引擎构建的,Firefox 使用 Geoko——Mozilla 自主研发的渲染引擎,Safari 和 Chrome 都使用 Webkit。Blink 是 Chrome 基于 WebKit的自主渲染引擎。

渲染的过程

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

图片描述

解析 HTML 以构建 Dom 树 -> 构建 Render 树 -> 布局 Render 树 -> 绘制 Render 树

构建 Dom 树

渲染现引擎的第一步是解析 HTML文档,并将解析后的元素转换为 DOM 树中的实际 DOM 节点。

假若有以下 Html 结构

<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>

对应的 DOM 树以下:

图片描述

基本上,每一个元素都表示为全部元素的父节点,这些元素直接包含在元素中。

构建 CSSOM

CSSOM 指的是 CSS 对象模型。 当浏览器构建页面的 DOM 时,它在 head 标签下如遇到了一个 link 标记且引用了外部 theme.css CSS 样式表。 浏览器预计可能须要该资源来呈现页面,它会当即发送请求。 假设 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 树,只显示咱们决定在样式表中覆盖的样式。 每一个浏览器都提供一组默认样式,也称为“user agent stylesheet”。这是咱们在未明确指定任何样式时看到的样式,咱们的样式会覆盖这些默认值。

图片描述

不一样浏览器对于相同元素的默认样式并不一致,这也是为何咱们在 CSS 的最开始要写 *{padding:0;marging:0};,也就是咱们要重置CSS默认样式的。

构建渲染树

CSSOM 树和 DOM 树链接在一块儿造成一个 render tree,渲染树用来计算可见元素的布局而且做为将像素渲染到屏幕上的过程的输入。

  • DOM 树和 CSSOM 树链接在一块儿造成 render tree .
  • render tree 只包含了用于渲染页面的节点
  • 布局计算了每个对象的准确的位置以及大小
  • 绘画是最后一步,绘画要求利用 render tree 来将像素显示到屏幕上

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

收下是上面 DOM 和 CSSOM 树的渲染器树的样子:

图片描述

为了构建渲染树,浏览器大体执行如下操做:

  • 从 DOM 树根节点开始,遍历每个可见的节点

    • 一些节点是彻底不可见的(好比 script标签,meta标签等),这些节点会被忽略,由于他们不会影响渲染的输出
    • 一些节点是经过 CSS 样式隐藏了,这些节点一样被忽略——例如上例中的 span 节点在 render tree 中被忽略,由于 span 样式是 display:none
  • 对每个可见的节点,找到合适的匹配的CSSOM规则,而且应用样式
  • 显示可见节点(节点包括内容和被计算的样式)
“visibility:hidden”“display:none” 之间的不一样, “visibility:hidden” 将元素设置为不可见,可是一样在布局上占领必定空间(例如,它会被渲染成为空盒子),可是 “display:none” 的元素是将节点从整个 render tree 中移除,因此不是布局中的一部分 。

你能够在这里查看 RenderObject 的源代码(在 WebKit 中):

https://github.com/WebKit/web...

咱们来看看这个类的一些核心内容:

图片描述

每一个渲染器表明一个矩形区域,一般对应于一个节点的 CSS 盒模型。它包含几何信息,例如宽度、高度和位置。

渲染树的布局

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

HTML使用基于流的布局模型,这意味着大多数时间它能够一次性计算几何图形。坐标系统相对于根渲染器,使用左上原点坐标。

布局是一个递归过程 - 它从根渲染器开始,它对应于 HTML 文档的 <html> 元素。 布局以递归方式继续经过部件或整个渲染器层次结构,为每一个须要它的渲染器计算几何信息。

根渲染器的位置为0,0,其尺寸与浏览器窗口的可见部分(即viewport)的大小相同。开始布局过程意味着给每一个节点在屏幕上应该出现的确切坐标。

绘制渲染树

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

绘图能够是全局的或增量式的(与布局相似):

  • 全局 — 整棵树被重绘
  • 增量式 — 只有一些渲染器以不影响整个树的方式改变。 渲染器使其在屏幕上的矩形无效,这会致使操做系统将其视为须要从新绘制并生成绘 paint 事件的区域。 操做系统经过将多个区域合并为一个来智能完成。

总的来讲,重要的中要理解绘图是一个渐进的过程。为了更好的用户体验,渲染引擎将尽量快地在屏幕上显示内容。它不会等到解析完全部 HTML 后才开始构建和布局渲染树,而是解析和显示部份内容,同时继续处理来自网络的其他内容项。

处理脚本和样式表的顺序

当解析器到达 <script> 标记时,将当即解析并执行脚本。文档的解析将暂停,直到执行脚本为止。这意味着这个过程是同步的

若是脚本是外部的,那么首先必须从网络中获取它(也是同步的)。全部解析都中止,直到获取完成。HTML5 新加了async 或 defer 属性,将脚本标记为异步的,以便由不一样的线程解析和执行。

优化渲染性能

若是你想优化本身的应用,则须要关注五个主要方面,这些是你本身能够控制的:

  1. JavaScript   — 在以前的文章中,讨论了若是编写优化代码的主题抱包括若是编写代码才不会阻止UI,和提升内存利用等等。在渲染时,须要考虑 JavaScript 代码与页面 上DOM 素交互的方式。 JavaScript 能够在 UI中建立大量更改,尤为是在 SPA 中。
  2. 样式计算 — 这是根据匹配选择器肯定哪一个 CSS 规则适用于哪一个元素的过程。 定义规则后,将应用它们并计算每一个元素的最终样式。
  3. 布局 — 一旦浏览器知道哪些规则适用于某个元素,它就能够开始计算后者占用多少空间以及它在浏览器屏幕上的位置。Web 的布局模型定义了一个元素能够影响其余元素。例如,<body> 的宽度会影响其子元素的宽度,等等。这意味着布局过程是计算密集型的,该绘图是在多个图层完成的。
  4. 绘图 —— 这是实际像素被填充的地方,这个过程包括绘制文本、颜色、图像、边框、阴影等——每一个元素的每一个可视部分。
  5. 合成  — 因为页面部分可能被绘制成多个层,所以它们须要以正确的顺序绘制到屏幕上,以便页面渲染正确。这是很是重要的,特别是对于重叠的元素。

优化你的 JavaScript

JavaScript 常常触发浏览器中的视觉变化,构建 SPA 时更是如此。

如下是一些优化 JavaScript 渲染技巧:

  • 避免使用 setTimeoutsetInterval 进行可视更新。 这些将在帧中的某个点调用 callback ,可能在最后。咱们想要作的是在帧开始时触发视觉变化而不是错过它。
  • 以前文章 所述,将长时间运行的 JavaScript 计算转移到 Web Workers。
  • 使用微任务在多个帧中变动 DOM。这是在任务须要访问 DOM 时使用的, Web Worker 没法访问 DOM。这基本上意味着你要把一个大任务分解成更小的任务,而后根据任务的性质在 requestAnimationFrame, setTimeout, setInterval 中运行它们。

优化你的 CSS

经过添加和删除元素,更改属性等来修改 DOM 将使浏览器从新计算元素样式,而且在许多状况下,从新计算整个页面的布局或至少部分布局。

要优化渲染,考虑如下事项:

  • 减小选择器的复杂性,与构造样式自己的其余工做相比,选择器复杂性能够占用计算元素样式所需时间的50%以上。

* 减小必须进行样式计算的元素的数量。本质上,直接对一些元素进行样式更改,而不是使整个页面无效。

优化布局

浏览器的布局从新计算可能很是繁重。 考虑如下优化:

  • 尽量减小布局的数量。当你更改样式时,浏览器会检查是否有任何更改须要从新计算布局。对宽度、高度、左、顶等属性的更改,以及一般与几何相关的属性的更改,都须要布局。因此,尽可能避免改变它们。
  • 尽可能使用 flexbox 而不是老的布局模型。它运行速度更快,可为你的应用程序创造巨大的性能优点。
  • 避免强制同步布局。须要记住的是,在 JavaScript 运行时,前一帧中的全部旧布局值都是已知的,能够查询。若是你访问 box.offsetHeight,那就不成问题了。可是,若是你在访问 box 以前更改了它的样式(例如,经过动态地向元素添加一些 CSS 类),浏览器必须先应用样式更改并执行布局过程,这是很是耗时和耗费资源的,因此尽量避免。

优化绘图

这一般是全部任务中运行时间最长的,所以尽量避免这种状况很是重要。 如下是咱们能够作的事情:

  • 除了变换(transform)和透明度以外,改变其余任何属性都会触发从新绘图,请谨慎使用。
  • 若是触发了布局,那也会触发绘图,由于更改布局会致使元素的视觉效果也改变。
  • 经过图层提高和动画编排来减小重绘区域。


原文:

https://blog.sessionstack.com...

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png