资源文件加载 async/defer/preload 实践篇

2,555 阅读3分钟

纸上得来终觉浅,绝知此事要躬行。

最近复习了下浏览器的加载过程,看到了关于资源文件的加载顺序。MDN 的浏览器的工作原理 里面说到 对于<script>标签(特别是没有 async 或者 defer 属性)会阻塞渲染并停止HTML的解析,说明有async、defer属性的script标签不会阻塞 html 的解析。具体加载顺序是什么样的呢,加了之后的加载顺序又是怎样的,同时有两个属性的加载顺序又是什么呢,我感觉很有必要实践一下。

MDN 上有说明如下

async 和 defer 属性值为 bool,它用来说明script脚本应该如何执行。在没有 src 属性的情况下,async 和 defer 属性可以不指定值。使用该属性有三种模式可供选择,如果async 属性存在,脚本将异步执行,只要它是可用的,如果 async 属性不存在,而 defer属性存在,脚本将会在页面完成解析后执行,如果都不存在,那么脚本会在 useragent 解析页面之前被取出并立刻执行。

预加载扫描器

引用自 MDN

浏览器构建DOM树时,这个过程占用了主线程。当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如CSS、JavaScript和web字体。多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用来请求它。

我们可以写一段代码来证明一般的同步 script 也是有预加载的。

<script>
  for (let i = 0; i < 10000000000; i++) {
    
  }
  console.log('html long time used', new Date().getTime())
</script>
<!-- defer 文件很大,但是全部都是注释 -->
<script type="text/javascript" src="./defer.js"></script>

刷新查看结果可以看到两个时间间隔仅有 19ms,但是 network 里面可以看到下载 defer.js 文件用了 3 秒,说明这个文件是有被提前下载的,而不是解析到 script标签才开始下载。

image.png image.png

async

当外部资源文件有 async 属性时,脚本是异步执行。测试一下。在 async 脚本后面加上一个加载时间很长的 js 文件。

<script type="text/javascript" async src="./async.js"></script>
<script type="text/javascript" src="./load-long.js"></script>
<script>
  console.log('load long')
</script>

可以看到 html 解析完成前,async 脚本就已经执行了。 image.png

defer

对于 defer 属性的脚本,需要等到页面解析完成后才执行。这时我们在 async 的脚本前加上 defer 脚本,即可测试 defer 的执行时间。

<script type="text/javascript" defer src="./defer.js"></script>
<script type="text/javascript" async src="./async.js"></script>
<script type="text/javascript" src="./load-long.js"></script>
<script>
  console.log('load long')
</script>

可以看到 defer 文件的执行是等到当前文件解析完成后才执行。

image.png

preload

前面 MDN 里面说预加载扫描器 会对字体进行预加载,一般我们使用字体文件是在样式中通过 @font-face 导入,这时候字体文件是不能预加载的。

image.png

这时我们通过 link 标签提前导入字体文件。需要添加 rel='preload' 和 as='font' 两个属性。添加了这两个属性后可以看到虽然字体文件预加载了,但是在 css 中字体文件还会重复加载。

<link href="./Ranchers-Regular.ttf" type="font/ttf" rel="preload" as="font">

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/34b3387edec04f118c28121c9f97bab7~tplv-k3u1fbpfcp-watermark.image)

原来还需要添加 crossorigin 属性,只有字体文件需要添加这个属性。

如果你已经有了一个可以正确工作的CORS设置,那么你也可以同样成功的预加载那些跨域资源,只需要你在元素中设置好crossorigin属性即可。一个有趣的情况是,如果你需要获取的是字体文件,那么即使是非跨域的情况下,也需要应用这一属性。因为各种各样的原因,这些获取请求必须使用以匿名模式使用CORS(如果你对其中的细节感兴趣,可以查看Font fetching requirements一文)。

<link href="./Ranchers-Regular.ttf" type="font/ttf" rel="preload" as="font" crossorigin="anonymous">

此时可以看到字体文件已经预加载而且也不会重复加载。

image.png

也可以看这个例子

另外 preload 只是会提前下载,并不会执行。所以该使用的地方照常使用,但是不会重复下载了。如果你提前下载了没有使用,console 里面会有警告提示没有用的 preload

prefetch

rel 还有一种属性类型 prefetch 也可以提前进行资源文件的下载。preload需要配合 as才会进行预加载,但是 prefetch不需要。

可以看下例子:

<link rel="prefetch" href="prefetch_1.js"></script>
<link rel="prefetch" href="prefetch_2.js"></script>

prefetch_1.js

console.log('this is prefetch 1 file');

可以看到 network 里面有下载这两个 js 文件,你在这两个 js 中打日志,会发现并没有执行。也就是可以提前下载,再次使用 script 标签使用的时候就不会重复下载。

image.png

当我们在 html 里面加入 script 引用 prefetch_1.js之后,在看下 network并没有变化,但是 console里面有日志了。

<link rel="prefetch" href="prefetch_1.js"></script>
<link rel="prefetch" href="prefetch_2.js"></script>
<script type="text/javascript" src="./prefetch_1.js"></script>

image.png

总结
  1. 对部分资源文件浏览器会预加载但是不会执行
  2. 没有 async/defer 属性的 js 会阻塞 html 解析
  3. async 属性的 js 会异步加载
  4. defer 属性的 js 会等待 html 解析完成在执行
  5. 对于浏览器没有预加载的资源可以通过 rel='preload' 和 as='xxx' 或者 rel='prefetch'强制预加载,字体文件需要添加 crossorigin="anonymous"。注意 rel='preload'或者rel='prefetch'只是下载了文件,并没有执行。另外因为提前下载会消耗 TCP 连接数,请考虑同时连接数,不能所有资源文件全部提前下载,只考虑必要的资源。
参考资料
  1. 对页面预解析进行优化
  2. 渲染页面:浏览器的工作原理
  3. 通过rel="preload"进行内容预加载
  4. link:外部资源链接元素
  5. Web 性能优化:Preload与Prefetch的使用及在 Chrome 中的优先级