更快地构建 DOM: 使用预解析, async, defer 以及 preload

图片 15

Web性能优化系列(1):Web性能优化分析

2015/04/08 · CSS,
HTML5,
JavaScript ·
性能优化

本文由 伯乐在线 –
鸭梨山大
翻译,sunbiaobiao
校稿。未经许可,禁止转载!
英文出处:gokulkrishh.github.io。欢迎加入翻译组。

如果你的网站在1000ms内加载完成,那么会有平均一个用户停留下来。2014年,平均网页的大小是1.9MB。看下图了解更多统计信息。

图片 1

网站的核心内容需要在1000ms内呈现出来。如果失败了,用户将永远不会再访问你的网站。通过降低页面加载的时间,很多著名公司的收入和下载量有显著的提升。比如

  • Walmart 每降低100ms的加载时间,
    他们的收入就提高1%.
  • Yahoo
    每降低400ms的加载时间,他们的访问量就提升9%。
  • Mozilla
    将他们的页面速度提升了2.2秒,每年多获得了1.6亿firefox的下载量。

更快地构建 DOM: 使用预解析, async, defer 以及 preload

2017/09/28 · JavaScript
· async,
defer,
DOM,
preload

原文出处: Milica
Mihajlija   译文出处:众成翻译   


2017年,保证页面快速加载的手段涵盖了方方面面,从压缩和资源优化,到缓存,CDN,代码分割以及
tree shaking 等。
然而,即便你不熟悉上面的这些概念,或者你感到无从下手,你仍然可以通过几个关键字以及精细的代码结构使得你的页面获得巨大的性能提升。

新的 Web 标准 “使你能够更快地加载关键资源,这个月晚些时候,Firefox
就会支持这个特性。同时在 Firefox
Nightly
版本或者 开发者版本
上已经可以使用这些功能。与此同时,这也是回顾基本原理,深入了解 DOM
解析相关性能的一个好时机。

理解浏览器的内部机制是每个 web
开发者最强有力的工具。我们看看浏览器是如何解释代码以及如何使用推测解析(speculative
parsing)来帮助页面快速加载的。我们会分析 deferasync
是如何生效的以及如何利用新的关键字 preload

网站优化的步骤

  1. 设定性能预算。
  2. 测试当前的性能。
  3. 找出导致性能问题的地方。
  4. 最后,duang,使用优化特技。

下面有几种方法可以提升你的页面性能,让我们来看看

构建模块

HTML 描述了一个页面的结构。为了理解
HTML,浏览器首先会将HTML转换成其能够理解的一种格式 –
文档对象模型(Document Object
Model)
或者简称为 DOM。
浏览器引擎有这么一段特殊的代码叫做解析器,用来将数据从一种格式转换成另外一种格式。一个
HTML 解析器就能将数据从 HTML 转换到 DOM。

在 HTML 当中,嵌套(nesting)定义了不同标签的父子关系。在 DOM
当中,对象被关联在树(一种数据结构)中用于捕获这些关系。每一个 HTML
标签都对应着树种的某个节点(DOM节点)。

浏览器一个比特一个比特地构建
DOM。一旦第一个代码块加载到浏览器当中,它就开始解析
HTML,添加节点到树中。

图片 2

DOM 扮演着两种角色:它既是 HTML
文档的对象表示,也充当着外界(比如JavaScript)和页面交互的接口。
当你调用 document.getElementById(),返回的元素是一个 DOM 节点。每个
DOM 节点都有很多函数可以用来访问和改变它,用户可以看到相应的变化。

图片 3

页面上的 CSS 样式被映射到 CSSOM 上 – CSS 对象模型(CSS Object
Model)。它就像
DOM,但是只针对于 CSS 而不是 HTML。不像 DOM,它不能增量地构建。因为 CSS
规则会相互覆盖,所以浏览器引擎要进行复杂的计算来确定 CSS 代码如何应用到
DOM 上。

图片 4

速度指标

速度指标是指页面的可视部分被呈现在浏览器中的平均速度。表示为毫秒的形式,并且取决于viewport的大小。请看下图(用视频帧的形式展现页面加载时间,以秒为单位)。

速度指标越低越好。

图片 5

速度指标可以通过Webpagetest
来测试(由Google维护)

关于“标签的历史

当浏览器构建 DOM 的时候,如果在 HTML 中遇到了一个
“标签,它必须立即执行。如果脚本是来自于外部的,那么它必须首先下载脚本。

在过去,为了执行一个脚本,HTML 的解析必须暂停。只有在 JavaScript
引擎执行完代码之后它才会重新开始解析。

图片 6

那位为什么解析必须要暂停呢?那是因为脚本可以改变 HTML以及它的产物 ——
DOM。 脚本可以通过 document.createElement()方法添加节点来改变 DOM
结构。为了改变
HTML,脚本可以使用臭名昭著的document.write()方法来添加内容。它之所以臭名昭著是因为它能以进一步影响
HTML 解析的方式来改变
HTML。比如,该方法可以插入一个打开的注释标签来使得剩余的 HTML
都变得不合法。

图片 7

脚本还可以查询关于 DOM 的一些东西,如果是在 DOM
还在在构建的时候,它可能会返回意外的结果。

图片 8

document.write()
是一个遗留的方法,它能够以预料之外的方式破坏你的页面,你应该避免使用它。处于这些原因,浏览器开发出了一些复杂的方法来应对脚本阻塞导致的性能问题,稍后我会解释。

长话短说

Webpage test 有很多特性,比如在不同的地方用不同的浏览器跑多个测试。
还可以测算其他的数据比如加载时间,dom元素的数量,首字节时间等等…

例如:查看amazon在webpagetest上的测试结果

可以看看这个视频,了解由
Patrick Meenan
讲解的关于webpagetest的更多信息(需要梯子)。

那么 CSS 会阻塞页面吗 ?

JavaScript 阻塞页面解析是因为它可以修改文档。CSS
不能修改文档,所以看起来它没有理由去阻塞页面解析,对吗?

那么,如果脚本需要样式信息,但样式还没有被解析呢?浏览器并不知道脚本要怎么执行——它可能会需要类似
DOM 节点的background-color
属性,而这个属性又依赖于样式表,或者它期望能够直接访问 CSSOM。

图片 9

正因为如此,CSS
可能会阻塞解析,取决于外部样式表和脚本在文档中的顺序。如果在文档中外部样式表放置在脚本之前,DOM
对象和 CSSOM 对象的构建可以互相干扰。 当解析器获取到一个 script
标签,DOM 将无法继续构建直到 JavaScript 执行完毕,而 JavaScript 在 CSS
下载完,解析完,并且 CSSOM 可以使用的时候,才能执行。

图片 10

另外一件要注意的事是,即使 CSS 不阻塞 DOM 的构建,它也会阻塞 DOM
的渲染。直到 DOM 和 CSSOM
准备好之前,浏览器什么都不会显示。这是因为页面没有 CSS
通常无法使用。如果一个浏览器给你显示了一个没有 CSS
的凌乱的页面,而几分钟之后又突然变成了一个有样式的页面,变换的内容和突然视觉变化使得用户体验变得非常糟糕。

具体可以参考由 Milica (@micikato) 在 CodePen 上制作的例子 —— Flash of
Unstyled Content。

这种糟糕的用户体验有一个名字 — Flash of Unstyled Content 或是 FOUC

为了避免这个问题,你应该尽快地呈现
CSS。记得流行的“样式放顶部,脚本放底部”的最佳实践吗?你现在知道它是怎么来的了!

渲染阻塞

如果你知道浏览器如何运行,那么你应该知道HTML, CSS,
JS是怎么被浏览器解析的以及其中哪个阻塞了页面的渲染。如果你不知道,请看下图。

图片 11

点击how a browser
works了解更多浏览器工作原理(作者为Tali
Garsiel 和Paul
Irish).

回到未来 – 预解析(speculative parsing)

每当解析器遇到一个脚本就暂停意味着每个你加载的脚本都会推迟发现链接到
HTML 的其他资源。

如果你有几个类似的脚本和图片要加载,例如:

<script src=”slider.js”></script> <script
src=”animate.js”></script> <script
src=”cookie.js”></script> <img src=”slide1.png”> <img
src=”slide2.png”>

1
2
3
4
5
  <script src="slider.js"></script>
  <script src="animate.js"></script>
  <script src="cookie.js"></script>
  <img src="slide1.png">
  <img src="slide2.png">

这个过程过去是这样的:

图片 12

这个状况在 2008 年左右改变了,当时 IE 引入了一个概念叫做 “先行下载”。
这是一种在同步的脚步执行的时候保持文件的下载的一种方法。Firefox,Chrome
和 Safari
随后效仿,如今大多数的浏览器都使用了这个技术,它们有着不同的名称。Chrome
和 Safari 称它为 “预扫描器” 而 Firefox 称它为预解析器。

它的概念是:虽然在执行脚本时构建 DOM 是不安全的,但是你仍然可以解析 HTML
来查看其它需要检索的资源。找到的文件会被添加到一个列表里并开始在后台并行地下载。当脚本执行完毕之后,这些文件很可能已经下载完成了。

上面例子的瀑布图现在看起来是这样的:

图片 13

以这种方式触发的下载请求称之为 “预测”,因为很有可能脚本还是会改变 HTML
结构(还记得document.write吗?),导致了预测的浪费。虽然这是有可能的,但是却不常见,所以这就是为什么预解析仍然能够带来很大的性能提升。

而且其他浏览器只会对链接的资源进行这样的预加载。在 Firefox 中,HTML
解析器对 DOM
树的构建也是算法预测的。有利的一面是,当推测成功的时候,就没有必要重新解析文件的一部分了。缺点是,如果推测失败了,就需要更多的工作。

浏览器渲染的步骤

  1. 首先浏览器解析HTML标记去构造DOM树(DOM = Document Object Model
    文档对象模型)
  2. 然后解析CSS去构造CSSOM树( CSSOM = CSS Object Model CSS对象模型)
  3. 在将DOM和CSSOM树结合成渲染树之前,JS文件被解析和执行。

现在你知道浏览器如何进行解析了,让我们看看是哪一部分阻塞了渲染树的生成。

关于(预)加载

这种资源加载的方式带来了显著地性能提升,你不需要做任何事情就可以使用这种优势。然而,作为一个
web 开发者,了解预解析是如何工作的能帮你最大程度地利用它。

可以预加载的东西在浏览器之间有所不同,但所有的主要的浏览器都会预加载:

  • 脚本
  • 外部 CSS
  • 来自 img 标签的图片

Firefox 也会预加载 video 元素的 poster 属性,而 Chrome 和 Safari
会预加载 @import 规则的内联样式。

浏览器能够并行下载的文件的数量是有限制的。这个限制在不同浏览器之间是不同的,并且取决于不同的因素,比如:你是否从同一个服务器或是不同的服务器下载所有的文件,又或者是你使用的是
HTTP/1.1 或是 HTTP/2
协议。为了更快地渲染页面,浏览器对每个要下载的文件都设置优先级来优化下载。为了弄清这些的优先级,他们遵守基于资源类型、标记位置以及页面渲染的进度的复杂方案。

在进行预解析时,浏览不会执行内联的 JavaScript
代码块。这意味着它不会发现任何的脚本注入资源,这些资源会排到抓取队列的最后面。

var script = document.createElement(‘script’); script.src =
“//somehost.com/widget.js”;
document.getElementsByTagName(‘head’)[0].appendChild(script);

1
2
3
4
var script = document.createElement(‘script’);
script.src = "//somehost.com/widget.js";
document.getElementsByTagName(‘head’)[0].appendChild(script);
 

你应该尽可能使浏览器能更轻松访问到重要的资源。你可以把他们放到 HTML
标签当中或者将要加载的脚本内联到文档的前面。然而,有时候需要一些不重要的资源晚一点被加载。这种情况,你通过
JavaScript 来加载他们来避免预解析。

你也可以看看这个 MDN
指南,里面讲述了如何针对预解析优化你的页面。

1. 阻塞渲染的CSS

有人认为CSS阻塞了渲染。在构造CSSOM时,所有的CSS都会被下载,无论它们是否在当前页面中被使用。

为了解决这个渲染阻塞,跟着下面的两个步骤做

  1. 将关键CSS内嵌入页面中,即将最重要的(首次加载时可见的部分页面所使用到的)style写入head中的
    <style></style>里。
  2. 移除没用到的CSS。

那么我是如何找出没用到的CSS的呢。

  1. 使用Pagespeed
    Insight
    去得到像未使用的CSS,阻塞渲染的CSS和JS文件等等的统计数据。例如:Flipkart的Pagespeed
    Insight统计结果。
  2. 使用Gulp任务,如gulp-uncss或是使用Grunt
    任务,如grunt-uncss。如果你不知道他们是什么,请阅读我之前的文章。

defer 和 async

不过,同步的脚本阻塞解析器仍旧是个问题。并不是所有的脚本对用户体验都是同等的重要,例如那些用于监测和分析的脚本。解决方法呢?就是去尽可能地异步加载这些不那么重要的脚本。

deferasync
属性
提供给开发者一个方式来告诉浏览器哪些脚本是需要异步加载的。

这两个属性都告诉浏览器,它可以 “在后台” 加载脚本的同时继续解析
HTML,并在脚本加载完之后再执行。这样,脚本下载就不会阻塞 DOM
构建和页面渲染了。结果就是,用户可以在所有的脚本加载完成之前就能看到页面。

deferasync 之间的不同是他们开始执行脚本的时机的不同。

deferasync
要先引入浏览器。它的执行在解析完全完成之后才开始,它处在DOMContentLoaded事件之前。
它保证脚本会按照它在 HTML 中出现的顺序执行,并且不会阻塞解析。

图片 14

async 脚本在它们完成下载完成后的第一时间执行,它处在 window
load
事件之前。 这意味着有可能(并且很有可能)设置了 async
的脚本不会按照它们在 HTML 中出现的顺序执行。这也意味着他们可能会中断 DOM
的构建。

无论它们在何处被指定,设置async
的脚本的加载有着较低的优先级。他们通常在所有其他脚本加载之后才加载,而不阻塞
DOM 构建。然而,如果一个指定async
的脚本很快就完成了下载,那么它的执行会阻塞 DOM
构建以及所有在之后才完成下载的同步脚。

图片 15

注: async 和 defer 属性只对外部脚本起作用,如果没有 src
属性它们会被忽略。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图