237 lines
10 KiB
Markdown
237 lines
10 KiB
Markdown
# 资源提示关键词
|
||
|
||
|
||
|
||
在上一篇文章中,我们介绍了浏览器的渲染流程,这篇文章中,我们将重点聚焦在渲染阻塞上,来详细看一下渲染阻塞以及一些常见的解决方法。
|
||
|
||
本文主要包含以下内容:
|
||
|
||
- 渲染阻塞回顾
|
||
- *defer* 和 *async*
|
||
- *preload*
|
||
- *prefetch*
|
||
- *prerender*
|
||
- *preconnect*
|
||
|
||
|
||
|
||
## 渲染阻塞回顾
|
||
|
||
我们都知道,*HTML* 用于描述网页的整体结构。为了理解 *HTML*,浏览器必须将它转为自己能够理解的格式,也就是 *DOM*(文档对象模型)
|
||
|
||
浏览器引擎有一段特殊的代码,称为解析器,用于将数据从一种格式转换为另一种格式。
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081458.png" alt="image-20211206161457653" style="zoom:50%;" />
|
||
|
||
浏览器一点一点地构建 *DOM*。一旦第一块代码进来,它就会开始解析 *HTML*,将节点添加到树结构中。
|
||
|
||

|
||
|
||
构建出来的 *DOM* 对象,实际上有 *2* 个作用:
|
||
|
||
- *HTML* 文档的结构以对象的方式体现出来,形成我们常说的 *DOM* 树
|
||
|
||
- 作为外界的接口供外界使用,例如 *JavaScript*。当我们调用诸如 *document.getElementById* 的方法时,返回的元素是一个 *DOM* 节点。每个 *DOM* 节点都有许多可以用来访问和更改它的函数,用户看到的内容也会相应地发生变化。
|
||
|
||
|
||
|
||

|
||
|
||
*CSS* 样式会被映射为 *CSSOM*( *CSS* 对象模型),它和 *DOM* 很相似,但是针对的是 *CSS* 而不是 *HTML*。
|
||
|
||
在构建 *CSSOM* 的时候,无法进行增量构建(不像构建 *DOM* 一样,解析到一个 *DOM* 节点就扔到 *DOM* 树结构里面),因为 *CSS* 规则是可以相互覆盖的,浏览器引擎需要经过复杂的计算才能弄清楚 *CSS* 代码如何应用于 *DOM*。
|
||
|
||

|
||
|
||
当浏览器正在构建 *DOM* 时,如果它遇到 *HTML* 中的 `<script>...</script>` 标记,它必须立即执行它。如果脚本是外部的,则必须先下载脚本。
|
||
|
||
过去,为了执行脚本,必须暂停解析。解析会在 *JavaScript* 引擎执行完脚本中的代码后再次启动。
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081717.png" alt="image-20211206161717368" style="zoom:50%;" />
|
||
|
||
|
||
|
||
为什么解析必须停止呢?
|
||
|
||
原因很简单,这是因为 *Javascript* 脚本可以改变 *HTML* 以及根据 *HTML* 生成的 *DOM* 树结构。例如,脚本可以通过使用 *document.createElement( )* 来添加节点从而更改 *DOM* 结构。
|
||
|
||

|
||
|
||
这也是为什么我们建议将 *script* 标签写在 *body* 元素结束标签前面的原因。
|
||
|
||
```html
|
||
<body>
|
||
<!-- HTML Code -->
|
||
<script>
|
||
JS Code...
|
||
</scirpt>
|
||
</body>
|
||
```
|
||
|
||
接下来我们回头来看一下 *CSS* 是否会阻塞渲染。
|
||
|
||
看上去 *JavaScript* 会阻止解析,是因为它可以修改文档。那么 *CSS* 不能修改文档,所以它似乎没有理由阻止解析,对吧?
|
||
|
||
但是,如果脚本中需要获取一些尚未解析的样式信息怎么办?在 *JavaScript* 中完全可以访问到 *DOM* 节点的某些样式,或者使用 *JavaScript* 直接访问 *CSSOM*。
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081801.png" alt="image-20211206161801072" style="zoom:50%;" />
|
||
|
||
因此,*CSS* 可能会根据文档中外部样式表和脚本的顺序阻止解析。如果在文档中的脚本之前放置了外部样式表,则 *DOM* 和 *CSSOM* 对象的构建可能会相互干扰。
|
||
|
||
当解析器到达一个脚本标签时,在 *JavaScript* 执行完成之前无法继续构建 *DOM*,然而如果这一段 *JavaScript* 中涉及到访问和使用 *CSSOM*,那么必须等待 *CSS* 文件被下载、解析并且 *CSSOM* 可用。如果 *CSSOM* 处于未可用状态,则会阻塞 *JavaScript* 的执行。
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081819.png" alt="image-20211206161819188" style="zoom:50%;" />
|
||
|
||
(上图中 *JavaScript* 的执行被 *CSS* 构建 *CSSOM* 的过程阻塞了)
|
||
|
||
|
||
|
||
另外,虽然 *CSS* 不会阻塞 *DOM* 的构建,但是也会阻塞渲染。
|
||
|
||
还记得我们前面有讲过要 *DOM* 树和 *CSSOM* 树都准备好,才会生成渲染树( *Render Tree* )么,浏览器在拥有 *DOM* 和 *CSSOM* 之前是不会显示任何内容。
|
||
|
||
这是因为没有 *CSS* 的页面通常无法使用。如果浏览器向你展示了一个没有 *CSS* 的凌乱页面,那么片刻之后就会进入一个有样式的页面,不断变化的内容和突然的视觉变化会给用户带来混乱的用户体验。
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081854.gif" alt="2021-11-22 15.59.41" style="zoom:50%;" />
|
||
|
||
|
||
|
||
(这种糟糕的用户体验有一个名字,叫做“无样式内容闪现”,*Flash of Unstyled Content*,或者简称 *FOUC* )
|
||
|
||
|
||
|
||
为了解决这些问题,所以我们需要尽快的交付 *CSS*。
|
||
|
||
这也解释了为什么“顶部样式,底部脚本”被称之为“最佳实践”。
|
||
|
||
随着现代浏览器的普及,浏览器为我们提供了更多强大的武器(资源提示关键词),合理利用,方可大幅提高页面加载速度。
|
||
|
||
|
||
|
||
## *defer* 和 *async*
|
||
|
||
现代浏览器引入了 *defer* 和 *async*。
|
||
|
||
*async* 表示加载和渲染后续文档元素的过程将和 *script.js* 的加载与执行并行进行(异步)。也就是说下载 *JS* 文件的时候不会阻塞 *DOM* 树的构建,但是执行该 *JS* 代码会阻塞 *DOM* 树的构建。
|
||
|
||
```html
|
||
<script async src="script.js"></script>
|
||
```
|
||
|
||
*defer* 表示加载后续文档元素的过程将和 *script.js* 的加载并行进行(异步),但是 *script.js* 的执行要在所有元素解析完成之后,*DOMContentLoaded* 事件触发之前完成。也就是说,下载 *JS* 文件的时候不会阻塞 *DOM* 树的构建,然后等待 *DOM* 树构建完毕后再执行此 *JS* 文件。
|
||
|
||
```html
|
||
<script defer src="myscript.js"></script>
|
||
```
|
||
|
||
具体加载瀑布图如下图所示:
|
||
|
||
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-08-032125.png" alt="image-20211208112125053" style="zoom:90%;" />
|
||
|
||
|
||
|
||
## *preload*
|
||
|
||
*preload* 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提前加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。
|
||
|
||
```html
|
||
<link rel="stylesheet" href="style2.css">
|
||
<script src="main2.js"></script>
|
||
|
||
<link rel="preload" href="style1.css" as="style">
|
||
<link rel="preload" href="main1.js" as="script">
|
||
```
|
||
|
||
在上面的代码中,会先加载 *style1.css* 和 *main1.js* 文件(但不会生效),在随后的页面渲染中,一旦需要使用它们,它们就会立即可用。
|
||
|
||
可以使用 *as* 来指定将要预加载的内容类型。
|
||
|
||

|
||
|
||
|
||
|
||
*preload* 指令的一些优点如下:
|
||
|
||
- 允许浏览器设置资源优先级,从而允许 *Web* 开发人员优化某些资源的交付。
|
||
|
||
- 使浏览器能够确定资源类型,因此它可以判断将来是否可以重用相同的资源。
|
||
|
||
- 浏览器可以通过引用 *as* 属性中定义的内容来确定请求是否符合内容安全策略。
|
||
|
||
- 浏览器可以根据资源类型发送合适的 *Accept* 头(例如:*image/webp* )
|
||
|
||
|
||
|
||
## *prefetch*
|
||
|
||
*prefetch* 是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制,通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度。
|
||
|
||
*prefetch* 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少 *5* 分钟(无论资源是否可以缓存)。并且,当页面跳转时,未完成的 *prefetch* 请求不会被中断;
|
||
|
||
它的用法跟 *preload* 是一样的:
|
||
|
||
```html
|
||
<link rel="prefetch" href="/path/to/style.css" as="style">
|
||
```
|
||
|
||
|
||
|
||
***DNS prefetching***
|
||
|
||
*DNS prefetching* 允许浏览器在用户浏览时在后台对页面执行 *DNS* 查找。这最大限度地减少了延迟,因为一旦用户单击链接就已经进行了 *DNS* 查找。
|
||
|
||
通过将 *rel="dns-prefetch"* 标记添加到链接属性,可以将 *DNS prefetching* 添加到特定 *URL*。建议在诸如 *Web* 字体、*CDN* 之类的东西上使用它。
|
||
|
||
```html
|
||
<!-- Prefetch DNS for external assets -->
|
||
<link rel="dns-prefetch" href="//fonts.googleapis.com">
|
||
<link rel="dns-prefetch" href="//www.google-analytics.com">
|
||
<link rel="dns-prefetch" href="//cdn.domain.com">
|
||
```
|
||
|
||
|
||
|
||
## *prerender*
|
||
|
||
*prerender* 与 *prefetch* 非常相似,*prerender* 同样也是会收集用户接下来可能会用到的资源。
|
||
|
||
不同之处在于 *prerender* 实际上是在后台渲染整个页面。
|
||
|
||
```html
|
||
<link rel="prerender" href="https://www.keycdn.com">
|
||
```
|
||
|
||
|
||
|
||
## *preconnect*
|
||
|
||
我们要讨论的最后一个资源提示是 *preconnect*。
|
||
|
||
*preconnect* 指令允许浏览器在 *HTTP* 请求实际发送到服务器之前设置早期连接。
|
||
|
||
我们知道,浏览器要建立一个连接,一般需要经过 *DNS* 查找,*TCP* 三次握手和 *TLS* 协商(如果是 *https* 的话),这些过程都是需要相当的耗时的。所以 *preconnet*,就是一项使浏览器能够预先建立一个连接,等真正需要加载资源的时候就能够直接请求了。
|
||
|
||

|
||
|
||
|
||
|
||
以下是为 *CDN URL* 启用 *preconnect* 的示例。
|
||
|
||
```html
|
||
<link href="https://cdn.domain.com" rel="preconnect" crossorigin>
|
||
```
|
||
|
||
在上面的代码中,浏览器会进行以下步骤:
|
||
|
||
- 解释 *href* 的属性值,判断是否是合法的 *URL*。如果是合法的 *URL*,然后继续判断 *URL* 的协议是否是 *http* 或者 *https*,如果不是合法的 *URL*,则结束处理。
|
||
- 如果当前页面 *host* 不同于 *href* 属性中的 *host*,那么将不会带上 *cookie*,如果希望带上 *cookie* 等信息,可以加上 *crossorign* 属性。
|
||
|
||
|
||
|
||
-------
|
||
|
||
|
||
|
||
-*EOF*-
|
||
|