likes
comments
collection
share

性能优化之script标签的async属性到底能不能在项目中用?有什么收益?

作者站长头像
站长
· 阅读数 17

背景

在性能优化中,我们都知道,async属性可以让script标签变得不阻塞HTML解析,defer属性也有类似的功能,但实际defer是会阻塞script解析的(用defer的话,多个script会按顺序执行,而async执行是无序的,谁下载的快执行谁),使用async属性理论上讲,是要比defer更快的。

用async属性有什么问题?

感觉不太安全,因为async会让多个script标签执行是无序的

实际测试async,看看是否是危险的

随便用一个脚手架起项目(vue或react都行),build之后,进入dist目录下,用http-server起服务,访问html如下(手动改成async)

<!doctype html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link rel="icon" href="/favicon.ico">
  <title>hello-world</title>
  <script async src="/js/chunk-vendors.f5ba3c0d.js"></script>
  <script async src="/js/app.382b3f67.js"></script>
  <link href="/css/app.2cf79ad6.css" rel="stylesheet">
</head>
<body>
  <div id="app"></div>
</body>
</html>

用弱网访问,看下的图,很明显 app.382b3f67.js 要先下载完(因为size小) 性能优化之script标签的async属性到底能不能在项目中用?有什么收益?

确认是否是 app.382b3f67.js 先执行, 手动往2个js里面埋入log代码,结果和预期一样,app.382b3f67.js先执行

性能优化之script标签的async属性到底能不能在项目中用?有什么收益?

确认 chunk-vendors.f5ba3c0d.js 里面的代码是关键依赖,比如是vue或react,理论上讲,应该是要报错的,因为依赖后执行了,但结果并没有报错

结论

在webpack项目内,可以放心对多个script标签使用async属性,webpack已经对产物做好了兼容

webpack是如何实现兼容async的?

分析打包出来的产物,可以探知到webpack是如何实现的

产物1:app.382b3f67.js (简化后的,方便理解)

(function () {
  "use strict";
  var __webpack_modules__ = ({
    4629:
      (function (__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) {}),
    6949:
      (function (module) {})
  });

  !function () {
    // ...
    var chunkLoadingGlobal = self["webpackChunkhello_world"] = self["webpackChunkhello_world"] || [];
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    // .push 被改写
    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
  }();
  var __webpack_exports__ = __webpack_require__.O(undefined, [998], function () {
    return __webpack_require__(4629);
  })
  __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
})();
console.log('app.382b3f67.js')

产物2:chunk-vendors.f5ba3c0d.js(简化后的,方便理解)

(self["webpackChunkhello_world"] = self["webpackChunkhello_world"] || []).push([[998], {
  // ...
}]);
console.log('chunk-vendors.f5ba3c0d.js')

解释:

  1. app.382b3f67.js里面的关键代码(webpack打包时的入口代码index.js),比如new Vue()或root.render(),被存放到了__webpack_require__(4629)里面(每次打包数字会变,不一定是4629)
  2. 4629 没有立即被执行,而是先存放好,等依赖[998]准备好,才执行

那app.382b3f67.js怎么知道依赖998已经准备好了呢?

  1. 依赖[998]就是chunk-vendors.f5ba3c0d.js,998被 .push 到了 self["webpackChunkhello_world"] 里面,这个push不是Array.push,而是被改写过的(我在上面写了注释,具体是哪一行被改写了)
  2. 改写的作用是:可以通知到 app.382b3f67.js,998依赖已经准备好了

由此可以得到结论:

  1. 依赖未准备好时,webpack打包时的入口代码index.js不会执行

到这里还没完,由上面的结论可以发现:如果不做特殊处理,我们几乎无法监控到webpack应用的白屏问题

  • 因为 如果某个js资源网络问题丢失了,index.js根本就不会执行,意味着上报代码也不会执行。如果js执行报错,那直接进程退出了,上报代码也不会执行(除非try catch了)

由此还可以得到一个结论:

  1. 如果要监控webpack应用的白屏问题,那么要用额外的script标签加载js来监控,这部分js不走webpack打包的逻辑

另外:附上webpack的版本:webpack@5.76.1


码字不易,点赞鼓励!!