落叶卢生
深度解析之异步加载(defer、async、module)和预加载(preload、prefetch、dns-prefetch、preconnect 、prerender)

废话:异步加载和预加载一直都是前端优化必备技能之一,今天我们就来深度解析一下常用的几个关键点。

异步加载

废话不多说,任何长篇大论的教程都抵不过一张清晰明了的高清大图来得好:

https://luoyelusheng.cn/wp-content/uploads/2019/03/async-2-1024x465.png

从这张图里面,我们看到了什么,大概总结为以下四点:

  • 默认情况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事件时机:

https://luoyelusheng.cn/wp-content/uploads/2019/03/load-1-1024x768.png

从这张图可以看出大概这几点:

  • 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的方法可能就会出现问题。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-1-1024x320.png

async

和defer一样,也仅仅适用于外部脚本,也就是仅当存在src属性时才会生效。

兼容性

async的兼容性在移动端也是一片大绿,IE仅支持IE10+。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-2-1024x324.png

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属性作为版本回退的方案解决。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-3-1024x285.png

最后来说一下module的使用建议,大型项目(100模块以上)不建议直接使用模块语法,应该使用打包工具诸如Webpack,Rollup,、或 Parcel,因为静态导入或导出语法是静态可分析的,通过捆绑工具可以去掉多余的模块,我们考虑下面这一种场景:

import { Modal } from './util.js';
Modal({
  title: 'hello'
})

如果我们通过打包工具打包这一份代码,最终生成的JS文件将会只包含Modal这一个函数,倘若我们没有使用打包工具,浏览器将会下载整个util这一个JS文件,并通过进一步分析了解了使用了Modal这一个函数,这对于没有用到util里面的全部函数的方式,则是一种多余的带宽浪费。

预加载

在我们的浏览器加载资源的时候,对于每一个资源都有其自身的默认优先级,倘若我们能修改每一个资源的默认优先级,那我们几乎可以按照我们的预期加载想要加载的资源。

以谷歌浏览器为例,我们打开控制台,并切换到Network选项,点击刷新页面,在网络下面的title一行点击鼠标右键,勾选Priority即可看到加载资源的优先级,我们可以看到样式的级别比脚本的优先级高,毕竟页面的一加载进来肯定是样式首先需要渲染的,不然整个页面便会四分五裂,用户体验不好。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-4-945x1024.png

preload

preload翻译过来就是预加载,一旦启用后便会告知浏览器应该尽快的加载某个资源,如果提取的资源3s内未在当前使用,在谷歌开发工具将会触发警告消息

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-5-1024x281.png

大概的语法如下:

<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+才能勉强支持,火狐需要手动启动支持,移动端支持程度还是挺好的。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-6-1024x317.png

prefetch

简而言之预提取就是在我们页面加载完成后,在带宽可用的情况下,加载用户下一步期待的页面资源,比如企业认证,一般都是分好几个页面进行认证的,在用户从第一个页面进行认证的时候,在页面加载完成,用户正在填写表单数据之时,加载第二个页面的部分资源,从而使用户更快打开下一个页面,从而增加用户体验,示例:

<link rel="prefetch" href="demo.html">
<link rel="stylesheet" href="demo.css">

当浏览器解析到link标签时,读取到rel的值为prefetch,便会将这一个资源添加的队列中,当浏览器空闲时便会预提取资源,但是在demo.html页面中只是加载HTML,不会加载demo页面里面的任何其他资源,除非你在demo页面也明确使用了预提取。

兼容性

各大浏览器支持都还挺好,IE11+以上,但是Safari貌似到现在还没支持。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-7-1024x351.png

dns-prefetch

我们都知道,当我们在浏览器的地址栏输入域名的时候,首先要进行的就是域名解析,因为我们需要加载域名对应的资源,这个过程很快,但是如果在移动端,那可是一个分秒必争的地方,当一个页面需要访问许多外部域名的资源的时候,如果我们能在用户浏览页面的时候,在浏览器空闲的时间,把可能需要访问的域名都提前做好了域名解析,那是不是大大增加了用户打开页面的响应时间,增加用户体验,为了解决这个问题,w3c便提出来一个标准,学名叫dns-prefetch。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-8-1024x281.png
dns-prefetch在淘宝上的使用

使用方法上面中已经支持了,指定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+以上部分兼容,移动端兼容良好。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-9-1024x322.png

prerender

预渲染,简单来说就是浏览器会下载指定链接的资源,并下载以及渲染它,就好比我们打开了一个新的Tab标签页,静默的在后台的下载执行,当然,浏览器也不一定会下载渲染它,这取决预很多情况,比如浏览器是否空闲以及操作系统是否会放弃下载过慢的资源文件。

除非你真的能十分的肯定用户接下来一定会触发你所指定的资源地址,否则对于用户来说这是一种带宽的浪费,使用例子如下:

<link rel="prerender" href="https://www.apple.com/">

兼容性

虽然是prerender是HTML5规范的一部分,但是似乎很多厂商都还没有实现,但是IE11竟然支持。

https://luoyelusheng.cn/wp-content/uploads/2019/03/image-10-1024x255.png

结尾

讲了这么多,最后整理了一个表格,帮助大家快速查阅参考,每个浏览器的实施细节都有所区别,这里以Chrome浏览器表格为例:

https://luoyelusheng.cn/wp-content/uploads/2019/04/屏幕快照-2019-04-02-上午11.20.10.png

参考:
[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)

赞赏
没有标签
首页      web前端      html      深度解析之异步加载(defer、async、module)和预加载(preload、prefetch、dns-prefetch、preconnect 、prerender)

发表评论

textsms
account_circle
email

  • https://secure.gravatar.com/avatar/7f927d35ad01a021a4291a69d3768334?s=80&d=retro&r=g

    好文章,谢谢分享,我的博客,欢迎回访

    3月前回复

落叶卢生

深度解析之异步加载(defer、async、module)和预加载(preload、prefetch、dns-prefetch、preconnect 、prerender)
废话:异步加载和预加载一直都是前端优化必备技能之一,今天我们就来深度解析一下常用的几个关键点。 异步加载 废话不多说,任何长篇大论的教程都抵不过一张清晰明了的高清大图来得…
扫描二维码继续阅读
2019-04-02