乐趣区

深度解析之异步加载和预加载

废话:异步加载和预加载一直都是前端优化必备技能之一,今天我们就来深度解析一下常用的几个关键点。
异步加载
废话不多说,任何长篇大论的教程都抵不过一张清晰明了的高清大图来得好:
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
从这张图里面,我们看到了什么,大概总结为以下四点:

默认情况 HTML 解析,然后加载 JS,此时 HTML 解析中断,然后执行 JS,最后 JS 执行完成恢复 HTML 解析。
defer 情况下 HTML 和 JS 并驾齐驱,最后才执行 JS
async 情况则 HTML 和 JS 并驾齐驱,JS 的执行可能在 HTML 解析之前就已经完成了
最后 module 情况和 defer 的情况类似,只不过会在提取的过程中加载多个 JS 文件罢了

好了,区分的大概基本已经了解了,那怎么记住呢?默认的情况我们已经很熟了,就无需多记了。
defer 翻译过来是延缓的意思,也就是拖拖拉拉了,所以比较懒,也就是说什么都不想做,也就是哪怕你把饭端在我面前,我也懒得动嘴的那种,这么一想,我们不就记住了,哪怕你客户端把 JS 文件下载好了,我也懒得执行,最后实在是大家都干完事了,我才不情愿的去执行 JS 文件。
async 翻译过来就是异步的意思,异步异步,不就是一步一步嘛,什么都想一步到位,也就是说,只要下载完我就立马执行,至于其他的想都不想。
module 翻译过来就是模块的意思,es6 用过的人基本都了解这个关键字,加载也和 defer 差不多,只不过可以加载多个 JS 文件而已。
我们再来看看这几个加载的 DOM 事件时机:
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
从这张图可以看出大概这几点:

async 会在加载完 JS 后立即执行,最迟也会在 load 事件前执行完。
defer 会在 HTML 解析完成后执行,最迟也会在 DOMContentLoaded 事件前执行完。

从上面我们可以看出,如果你的脚本依赖于 DOM 构建完成是否完成,则可以使用 defer;如果无需 DOM 的构建,那就可以放心的使用 async 了。
defer
defer 属性仅适用于外部脚本,也就是仅当存在 src 属性时才会生效;如果一个 script 标签上面即存在 defer 属性,也存在 async 属性,那么浏览器会如何解析这种情况呢?我们通过一段代码验证结果, 详情点击这里。
也就是说 defer 的优先级没有 async 高,我们看一下规范是怎么处理这种情况的。
The defer attribute may be specified even if the async attribute is specified, to cause legacy Web browsers that only support defer (and not async) to fall back to the defer behavior instead of the blocking behavior that is the default.
规范只是说明了在不支持 async 的情况下浏览器将会回退支持 defer,但并没有明确指明两种都支持的这种情况,也就是说这一种情况浏览器自行处理,经过测试,各个浏览器表现行为:

Chrome 浏览器表现为解析为 async 特性
Safari 浏览器表现为 async 特性
Opera 浏览器表现为 async 特性
Firefox 浏览器表现为 async 特性

IE 暂时没有安装,看来各大浏览器表现一致,总之 async 的优先级是最高的。
兼容性
下面来看看 defer 的兼容性,移动端一片大绿,可以放心使用,IE10 以上可以放心使用,IE6- 9 有一点小问题就是不会按照 script 标签的执行顺序进行执行,对于不依赖前后脚本库的可以不用担心,但是如果依赖库的就不行了,比如你的项目依赖 jQuery,后面紧接着使用 jQuery 的方法可能就会出现问题。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
async
和 defer 一样,也仅仅适用于外部脚本,也就是仅当存在 src 属性时才会生效。
兼容性
async 的兼容性在移动端也是一片大绿,IE 仅支持 IE10+。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
module
在现代浏览器中,我们可以声明 acript 标签 type=’module’属性从而拥抱 es6 的模块导入导出语法,就像这样:
<script type=”module”>
import {Max} from “./math.js”;
console.log(Max(1, 2, 7, 2, 0)); //7
</script>
看起来是不是令人很激动,似乎对于开发者十分友好,但是这里也有几个与传统脚本不一样的地方:

module 默认使用了”use strict”模式,这也意味着不能使用诸如 arguments.callee 这一类的语法。
模块只会加载一次,无论前后你写了多少次。
不支持 <!–const a = 1–> 注释。
module 有自己的词法作用域,比如定义一个 var a = 1,并不会创建一个全局变量,因此你并不能通过 window.a 访问到它的值。

模块的导入方式目前仅支持以下几种模式:
支持
import {math} from ‘./math.mjs’;
import {math} from ‘../math.mjs’;
import {math} from ‘/modules/math.mjs’;
import {math} from ‘https://simple.example/modules/math.mjs’;
// 不支持
import {math} from ‘jquery’;
当然,浏览器厂商也在考虑支持 import {math} from‘jquery’这种格式,不过,还是需要一段很长的路要走。
module 的默认情况就是 defer 的,因此不必再 module 上面又添加一个 defer 熟悉,并且本身就不支持这种写法,但是支持 async 属性,其加载渲染方式和 async 差不多,这里不再赘述。
兼容性
在移动端的兼容性还算可以,但是 IE 貌似都败下阵来,只要 edge16+ 以上还算支持,对于不支持 module 的浏览器可以使用 nomodule 属性作为版本回退的方案解决。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
最后来说一下 module 的使用建议,大型项目(100 模块以上)不建议直接使用模块语法,应该使用打包工具诸如 Webpack,Rollup,、或 Parcel,因为静态导入或导出语法是静态可分析的,通过捆绑工具可以去掉多余的模块,我们考虑下面这一种场景:
import {Modal} from ‘./util.js’;
Modal({
title: ‘hello’
})
如果我们通过打包工具打包这一份代码,最终生成的 JS 文件将会只包含 Modal 这一个函数,倘若我们没有使用打包工具,浏览器将会下载整个 util 这一个 JS 文件,并通过进一步分析了解了使用了 Modal 这一个函数,这对于没有用到 util 里面的全部函数的方式,则是一种多余的带宽浪费。
预加载
在我们的浏览器加载资源的时候,对于每一个资源都有其自身的默认优先级,倘若我们能修改每一个资源的默认优先级,那我们几乎可以按照我们的预期加载想要加载的资源。
以谷歌浏览器为例,我们打开控制台,并切换到 Network 选项,点击刷新页面,在网络下面的 title 一行点击鼠标右键,勾选 Priority 即可看到加载资源的优先级,我们可以看到样式的级别比脚本的优先级高,毕竟页面的一加载进来肯定是样式首先需要渲染的,不然整个页面便会四分五裂,用户体验不好。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
preload
preload 翻译过来就是预加载,一旦启用后便会告知浏览器应该尽快的加载某个资源,如果提取的资源 3s 内未在当前使用,在谷歌开发工具将会触发警告消息
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
大概的语法如下:
<link rel=”preload” as=”script” href=”foo.js”>
<link rel=”preload” as=”style” href=”bar.css”>
除了以上指定的资源外,还可以加载 audio、font、video 以及 document 等,详情点击这里了解。
跨域资源
如需加载跨域的资源列表,则需要正确设置 CORS,接着便可以在 <link> 元素中设置好 crossorigin 属性即可:
<link rel=”preload” as=”font” crossorigin=”crossorigin” type=”font/woff2″ href=”foo.woff2″>
这里有一个特例便是无论是否跨域,字体的获取都需要设置 crossorigin 属性,这是由于历史原因造成,有兴趣了解可移步这里了解,另外我们还可以使用 media 响应式的加载图片,比如:
<link rel=”preload” href=”bg@2x.png” as=”image” media=”(max-width: 325px)”>
<link rel=”preload” href=”bg@3x.png” as=”image” media=”(min-width: 400px)”>
另一个重要的地方便是如果预加载一个脚本,它并不是执行:
// 只拉取下载不执行
var preloadLink = document.createElement(“link”);
preloadLink.href = “foo.js”;
preloadLink.rel = “preload”;
preloadLink.as = “script”;
document.head.appendChild(preloadLink);

// 如果需要执行
var preloadedScript = document.createElement(“script”);
preloadedScript.src = “foo.js”;
document.body.appendChild(preloadedScript);
兼容性
兼容似乎 IE 全体阵亡,edge 也得 17+ 才能勉强支持,火狐需要手动启动支持,移动端支持程度还是挺好的。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
prefetch
简而言之预提取就是在我们页面加载完成后,在带宽可用的情况下,加载用户下一步期待的页面资源,比如企业认证,一般都是分好几个页面进行认证的,在用户从第一个页面进行认证的时候,在页面加载完成,用户正在填写表单数据之时,加载第二个页面的部分资源,从而使用户更快打开下一个页面,从而增加用户体验,示例:
<link rel=”prefetch” href=”demo.html”>
<link rel=”stylesheet” href=”demo.css”>
当浏览器解析到 link 标签时,读取到 rel 的值为 prefetch,便会将这一个资源添加的队列中,当浏览器空闲时便会预提取资源,但是在 demo.html 页面中只是加载 HTML,不会加载 demo 页面里面的任何其他资源,除非你在 demo 页面也明确使用了预提取。
兼容性
各大浏览器支持都还挺好,IE11+ 以上,但是 Safari 貌似到现在还没支持。

dns-prefetch
我们都知道,当我们在浏览器的地址栏输入域名的时候,首先要进行的就是域名解析,因为我们需要加载域名对应的资源,这个过程很快,但是如果在移动端,那可是一个分秒必争的地方,当一个页面需要访问许多外部域名的资源的时候,如果我们能在用户浏览页面的时候,在浏览器空闲的时间,把可能需要访问的域名都提前做好了域名解析,那是不是大大增加了用户打开页面的响应时间,增加用户体验,为了解决这个问题,w3c 便提出来一个标准,学名叫 dns-prefetch。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
使用方法上面中已经支持了,指定 rel=”dns-prefetch”,在 href 中指定页面需要解析的域名即可,你可能已经注意到了上面的图中域名使用了双斜杠,这个双斜杠表示 URL 以主机名开头,和你使用完整 URL(比如 http://g.alicdn.com/)是等效的。在 RFC1808 中被指定。
当然并不是所有的页面需要用到的外部域名都需要做这样的域名解析,浏览器默认会解析超链接属性的 href 里面的域名,并且你的网站域名还不能是 HTTPS,如果是 HTTPS,则需要设置请求头或加入一段强制开启域名解析的 meta 标签。
//HTTP
<link rel=”dns-prefetch” href=”//a.com”> // 多余
<a href=”http://a.com”>
//HTTPS
<meta http-equiv=”x-dns-prefetch-control” content=”on”>// 强制开启
<a href=”http://a.com”>
当然,并不建议对 HTTPS 网站开启强制解析的方式,因为这样会带来一些安全隐患,具体可参考这里。
preconnect
预连接,也就是启动早期连接(包括 DNS 查找,TCP 握手和可选 TLS 协商),我们来看一个例子:
<link href=’https://fonts.demo.com’ rel=’preconnect’ crossorigin>
<link href=’https://demo.com/css?family= 黑体 ’ rel=’stylesheet’>
一个网络字体正常加载一般都包括:

页面加载样式,解析样式用到的网络字体
网络字体开始下载,首先开始 DNS 的查找
然后 TCP 握手
如果是 HTTPS,还有 TLS 协商,最后下载字体

差不多一个字体的渲染要经过这么几个过程,但是如果字体的前期准备(DNS 查找,TCP 握手和可选 TLS 协商)和样式的加载是并行执行,是不是可以更快的渲染页面,preconnect 就是为这个而生的,从而优化用户体验。
当然如果是跨域资源,不要忘了加上 crossorigin 属性。
兼容性
IE15+ 以上部分兼容,移动端兼容良好。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
prerender
预渲染,简单来说就是浏览器会下载指定链接的资源,并下载以及渲染它,就好比我们打开了一个新的 Tab 标签页,静默的在后台的下载执行,当然,浏览器也不一定会下载渲染它,这取决预很多情况,比如浏览器是否空闲以及操作系统是否会放弃下载过慢的资源文件。
除非你真的能十分的肯定用户接下来一定会触发你所指定的资源地址,否则对于用户来说这是一种带宽的浪费,使用例子如下:
<link rel=”prerender” href=”https://www.apple.com/”>
兼容性
虽然是 prerender 是 HTML5 规范的一部分,但是似乎很多厂商都还没有实现,但是 IE11 竟然支持。
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
结尾
讲了这么多,最后整理了一个表格,帮助大家快速查阅参考,每个浏览器的实施细节都有所区别,这里以 Chrome 浏览器表格为例:
和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)”)
参考:[1] https://www.w3.org/TR/resource-hints/#prerender[2] https://dev.chromium.org/developers/design-documents/dns-prefetching[3] 资源优先级 – 让浏览器助您一臂之力[4] JavaScript Loading Priorities in Chrome[5] Chrome Resource Priorities and Scheduling[6] Using JavaScript modules on the web[7] https://www.w3.org/TR/html5/webappapis.html#module-script
原文出处:深度解析之异步加载 (defer、async、module) 和预加载(preload、prefetch、dns-prefetch、preconnect、prerender)

退出移动版