Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

  • 工程能力

  • 应用基础

    • 兼容性

    • 前端安全

    • 国际化

    • 性能优化

      • 如何实现 script 并行异步加载顺序执行
        • 前言
        • 异步加载的手段
          • 1. 最简单的做法:Script Dom
          • 存在的问题:
          • 2. script onload后再发起请求,按序下载-执行
          • 存在的问题:
          • 3. XHR+eval
          • 存在的问题:
          • 4. object tag 预加载资源,script onload 按序下载(命中缓存)-执行
          • 存在的问题:
          • 5. new Image().src 预加载资源,script onload 按序下载(命中缓存)-执行
          • 存在的问题:
          • 补充:
          • 6. 最终方案
          • LABjs v3.0的方案:
          • 优化
        • 参考文档
      • yahoo前端优化35军规
      • 前端首屏优化 | 借助客户端能力提升 H5 首屏的 8 个手段
      • 前端优化性能指标
      • 4 种常用的前端性能分析工具
      • 前端性能预算经验
      • 前端图片分层加载详解
      • 基于CDN的前端优化可行性分析
      • 浅谈图片分层加载与懒加载
      • 浅谈 preload 预加载
      • 用 CAP 理论指导 Hybrid App 离线策略优化
      • 长列表

      • 面试官问:如何实现 H5 秒开?
    • 换肤

    • 无障碍

  • 专业领域

  • 业务场景

  • 大前端
  • 应用基础
  • 性能优化
gahing
2018-08-10
目录
前言
异步加载的手段
1. 最简单的做法:Script Dom
存在的问题:
2. script onload后再发起请求,按序下载-执行
存在的问题:
3. XHR+eval
存在的问题:
4. object tag 预加载资源,script onload 按序下载(命中缓存)-执行
存在的问题:
5. new Image().src 预加载资源,script onload 按序下载(命中缓存)-执行
存在的问题:
补充:
6. 最终方案
LABjs v3.0的方案:
优化
参考文档

如何实现 script 并行异步加载顺序执行

# 前言

前端优化有个原则,叫资源懒加载。

对于某些js资源,我们在页面load前并不需要用到,加载反而会影响到首屏速度。

把这些js放到 load 后进行加载,我们称之为js异步加载。

# 异步加载的手段

# 1. 最简单的做法:Script Dom

var script = document.createElement("script")
script.src="xxx.js"
document.head.appendChild(script)
1
2
3

多个js我们进行循环即可

# 存在的问题:

  1. 大部分浏览器不会顺序执行script,(firefox、opera某些版本可以),对于有依赖的脚本会出现各种未定义错误和逻辑错误

# 2. script onload后再发起请求,按序下载-执行

// 顺序下载和执行
AsyncLoad.sync = (function () {
  /**
   * 加载js并放入执行队列中
   * 
   * @param {string} url 
   * @param {string} [type="normal"] script类型,normal为普通js此外还有async、defer
   * @param {function} callback 
   */
  var normalQueue = []
  var deferQueue = []
  var processedNum = 0
  function loadScript(url, type, callback) {
    type = type || 'normal'
    switch (type) {
      case 'defer':
        var dqId = deferQueue.length
        //cached: <object>缓存成功 done: 是否执行script成功
        deferQueue[dqId] = { url: url, cached: false, done: false, onload: callback }
        break;
      case 'async':
        var script = document.createElement('script')
        script.onload = function () {
          if (callback) {
            callback();
          }
        }
        script.src = url
        document.head.appendChild(script)
        break;
      default:
        var nqId = normalQueue.length
        normalQueue[nqId] = { url: url, cached: false, done: false, onload: callback }
        break;
    }

  }
  //顺序执行
  function processScripts() {
    if (deferQueue.length > 0) {
      normalQueue = normalQueue.concat(deferQueue)
      deferQueue = []
    }
    // 遇到有src的就中断执行
    if (processedNum < normalQueue.length) {
      var head = document.head;
      var newScript = document.createElement('script');
      newScript.type = 'text/javascript';
      newScript.src = normalQueue[processedNum].url;
      newScript.onload = function () {
        processScripts();
      }
      newScript.onerror=newScript.onload
      processedNum++;
      head.appendChild(newScript);
    }
  };
  return {
    loadScript: loadScript,
    processScripts: processScripts
  }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

用法(后面的方法就把 sync 换成各自方法名):

   var AL = AsyncLoad.sync
   AL.loadScript('./js/d.js', 'defer', () => console.log("defer 1 加载完毕"))
   AL.loadScript('./js/a.js', 'normal', () => console.log("a.js加载完毕"))
   AL.loadScript('./js/b.js', '', () => console.log("async 1 加载完毕")) 
1
2
3
4

# 存在的问题:

  1. 与浏览器页面解析时的并发下载顺序执行逻辑不同,不能充分利用IO/CPU的并行操作,耗时会较久。且当出现某个资源请求较久时会影响会更严重。

# 3. XHR+eval

利用ajax请求js数据,保存响应内容,并按序eval。可以做到并行下载,按序执行

AsyncLoad.xhr = (function () {

  var queuedScripts = []
  function loadScript(url, type, onload) {
    type = type || 'normal'
    var iQ = queuedScripts.length;

    //如果需要按顺序执行,并将脚本对象放入数组
    if (type !== 'async') {
      var qScript = { response: null, onload: onload, done: false };
      queuedScripts[iQ] = qScript;
    }

    //调用AJAX
    var xhrObj = getXHRObject();
    xhrObj.onreadystatechange = function () {
      if (xhrObj.readyState == 4) {


        if (type !== 'async') {
          queuedScripts[iQ].response = xhrObj.responseText;
          injectScripts();

          //如果不需要按顺序执行,即立即加载脚本
        } else {
          eval(xhrObj.responseText);
          if (onload) {
            onload();
          }
        }
      }
    };
    xhrObj.open('GET', url, true);
    xhrObj.send('');
  }
  function injectScripts() {
    var len = queuedScripts.length;
    for (var i = 0; i < len; i++) {
      var qScript = queuedScripts[i];

      //已加载的脚本
      if (!qScript.done) {

        //如果响应未返回 立即停止
        if (!qScript.response) {
          break;

          //执行脚本
        } else {
          eval(qScript.response);
          if (qScript.onload) {
            qScript.onload();
          }
          qScript.done = true;
        }
      }
    }
  }
  //AJAX对象
  function getXHRObject() {
    var xhrObj = false;
    try {
      xhrObj = new XMLHttpRequest();
    }
    catch (e) {
      var aTypes = ["Msxm12.XMLHTTP6.0",
        "Msxm12.XMLHTTP3.0",
        "Msxm12.XMLHTTP",
        "Microsoft.XMLHTTP"];
      var len = aTypes.length;
      for (var i = 0; i < len; i++) {
        try {
          xhrObj = new ActiveXObject(aTypes[i]);
        }
        catch (e) {
          continue;
        }
        break;
      }
    }
    finally {
      return xhrObj;
    }
  }
  return {
    loadScript: loadScript,
    processScripts:()=>{}
  }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

# 存在的问题:

  1. 跨域问题

# 4. object tag 预加载资源,script onload 按序下载(命中缓存)-执行

contorl.js二次命中缓存实现并行下载顺序执行,但是它通过setTimeout查询是否执行完毕,比我的实现差点

AsyncLoad.object = (function () {
  /**
   * 加载js并放入执行队列中
   * 
   * @param {string} url 
   * @param {string} [type="normal"] script类型,normal为普通js此外还有async、defer
   * @param {function} callback 
   */
  var normalQueue = []
  var deferQueue = []
  var isExecuting = false //dom插入script到script执行完毕这段过程 取值为true
  var waitNum = 0 // 待执行injectScripts的个数
  function loadScript(url, type, callback) {
    type = type || 'normal'
    switch (type) {
      case 'defer':
        var dqId = deferQueue.length
        //cached: <object>缓存成功 done: 是否执行script成功
        deferQueue[dqId] = { url: url, cached: false, done: false, onload: callback }
        preload(deferQueue[dqId])
        break;
      case 'async':
        var script = document.createElement('script')
        script.onload = function () {
          if (callback) {
            callback();
          }
        }
        script.src = url
        document.head.appendChild(script)
        break;
      default:
        var nqId = normalQueue.length
        normalQueue[nqId] = { url: url, cached: false, done: false, onload: callback }
        preload(normalQueue[nqId])
        break;
    }

  }
  /**
   * 
   * 
   * 
   * @param {any} item 队列元素
   */
  function preload(item) {
    //chrome会出现Resource interpreted as Document but transferred with MIME type application/javascript警告
    var obj = document.createElement('object');
    // console.log(item.url, 'preload...')
    obj.onload = function () {
      // console.log(item.url, 'object cached...', isExecuting, waitNum)
      //触发script标签插入
      item.cached = true
      obj.onload = null
      if (isExecuting) {
        waitNum++
      } else {
        waitNum += injectScripts()
      }

    }
    obj.onerrot = obj.onload
    obj.data = item.url
    obj.width = 1;
    obj.height = 1;
    obj.style.visibility = "hidden";
    obj.type = "text/plain";
    document.body.appendChild(obj)
  }
  function injectScripts() {
    if (deferQueue.length > 0) {
      normalQueue = normalQueue.concat(deferQueue)
      deferQueue = []
    }
    var num = 1
    for (var i = 0; i < normalQueue.length; i++) {
      var normal = normalQueue[i];
      if (!normal.done) {
        if (normal.cached) {
          num = 0
          syncExcuteScript(normal)
        }
        break;
      }
    }
    return num
  }
  function syncExcuteScript(item) {
    // console.log(item.url, 'pre insertScript...', isExecuting, waitNum)
    isExecuting = true
    var script = document.createElement("script")
    script.onload = function () {
      isExecuting = false
      // console.log(item.url, 'completed', isExecuting, waitNum)
      script.onload = null
      item.done = true
      if (item.onload) {
        item.onload();
      }
      if (waitNum) {
        waitNum--
        waitNum += injectScripts()
      }
    }
    script.onerror = script.onload
    script.src = item.url
    document.head.appendChild(script)
  }
  return {
    loadScript: loadScript,
    processScripts:()=>{}
  }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

# 存在的问题:

  1. 创建<object/>后,需要插入文档才会发起请求(dom操作耗时),浏览器还会构建一个blob对象(挺耗时的),且即使命中本地缓存也有些许耗时,总体并没有方法2 顺序下载执行来的快
  2. 不论是否有缓存,都会发起两次请求,当js全被缓存的时候,该做法比方法2慢的多

# 5. new Image().src 预加载资源,script onload 按序下载(命中缓存)-执行

相比object tag的一个好处是不需要进行dom操作,不用构建blob对象,总体速度比前几种方法都快

AsyncLoad.img = (function () {
  /**
   * 加载js并放入执行队列中
   * 
   * @param {string} url 
   * @param {string} [type="normal"] script类型,normal为普通js此外还有async、defer
   * @param {function} callback 
   */
  var normalQueue = []
  var deferQueue = []
  var isExecuting = false //dom插入script到script执行完毕这段过程 取值为true
  var waitNum = 0 // 待执行injectScripts的个数
  function loadScript(url, type, callback) {
    type = type || 'normal'
    switch (type) {
      case 'defer':
        var dqId = deferQueue.length
        //cached: <object>缓存成功 done: 是否执行script成功
        deferQueue[dqId] = { url: url, cached: false, done: false, onload: callback }
        preload(deferQueue[dqId])
        break;
      case 'async':
        var script = document.createElement('script')
        script.onload = function () {
          if (callback) {
            callback();
          }
        }
        script.src = url
        document.head.appendChild(script)
        break;
      default:
        var nqId = normalQueue.length
        normalQueue[nqId] = { url: url, cached: false, done: false, onload: callback }
        preload(normalQueue[nqId])
        break;
    }

  }
  /**
   * 
   * 
   * 
   * @param {any} item 队列元素
   */
  function preload(item) {
    //chrome会出现Resource interpreted as Document but transferred with MIME type application/javascript警告
    var img = new Image();
    console.log(item.url, '预加载')
    img.onload = function () {
      console.log(item.url, 'img cached 结束', isExecuting, waitNum)
      //触发script标签插入
      item.cached = true
      img.onload = null
      if (isExecuting) {
        waitNum++
      } else {
        waitNum += injectScripts()
      }

    }
    img.onerror = img.onload
    img.src = item.url
  }
  function injectScripts() {
    
    if (deferQueue.length > 0) {
      console.log('normalQueue.concat(deferQueue)',JSON.stringify(deferQueue))
      normalQueue = normalQueue.concat(deferQueue)
      deferQueue = []
    }
    var num = 1
    for (var i = 0; i < normalQueue.length; i++) {
      var normal = normalQueue[i];
      if (!normal.done) {
        if (normal.cached) {
          num = 0
          syncExcuteScript(normal)
        }
        break;
      }
    }
    return num
  }
  function syncExcuteScript(item) {
    console.log(item.url, '预插入<script>', isExecuting, waitNum)
    isExecuting = true
    var script = document.createElement("script")
    script.onload = function () {
      isExecuting = false
      console.log(item.url, 'js 执行完毕', isExecuting, waitNum)
      script.onload = null
      item.done = true
      if (item.onload) {
        item.onload();
      }
      if (waitNum) {
        waitNum--
        waitNum += injectScripts()
      }
    }
    script.onerror = script.onload
    script.src = item.url
    document.head.appendChild(script)
  }
  return {
    loadScript: loadScript,
    processScripts:()=>{console.log(normalQueue)}
  }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

# 存在的问题:

  1. 不论是否有缓存,都会发起两次请求(尽管第二次是命中本地缓存),当js全被缓存的时候,该做法比方法2稍慢;
  2. 浏览器设置禁用缓存时该方案更慢
  3. 出现过img请求某些js时响应不完整,导致第二次请求不走缓存仍是完整请求 (留个坑,具体原因待分析)

第二次其实不是完整请求,响应码为206表示返回部分内容,应该是和第一次请求进行合并处理。具体技术细节还不清楚,但目前来看走的流量并不会多。

# 补充:

浏览器开发者工具开启 Disable cache后,任何请求都不会走本地强缓存,但是会走304协商缓存(强制刷新除外)
未开启Disable cache的状态下,ctrl+F5强制刷新,对于page load前的请求,都是不走缓存(强缓存和协商缓存cache-control:no-cache)的,但是page load后的请求不受限制可以走缓存

# 6. 最终方案

# LABjs v3.0的方案:

  1. 对于支持<link rel="preload" href="xxx.js" as="script">的浏览器【chrome50+、safari 11+】,则用preload进行预加载(请求会复用,不用担心与script.src同时发起会发两个请求),只要支持就加上
  2. 对于支持async的,即document.createElement("script").async === true【IE>=10 ,其他浏览器大部分版本】,在方案1的基础上设置一个script.async=false即可
  3. 对于其他浏览器,采用方案2做法

PS: preload 和 prefetch 的区别可以参考:Preload,Prefetch 和它们在 Chrome 之中的优先级

PS2: preload预加载后,插入script节点不会发起请求,不是命中本地缓存的方式(200 from cache)。也就是说即使禁用缓存,后续也不会进行重复请求!

# 优化

正常来说,async=false即可解决大部分浏览器,剩下的就是IE9版本及以下,以及其他浏览器的某些版本

本方案会利用IE的特性优化方案2做法,实现并行下载按序执行:IE系列 设置script.src 后即发起请求,插入dom才执行

对于不支持async属性、async=false不会按序执行的(如Safari 5.0),则利用方案5做法。

从产品层面考虑,若担心方案5用户禁用缓存导致的双倍流量,则采用xhr预加载同域js eval+方案2。

# 参考文档

  1. 异步加载脚本保持执行顺序
  2. Deep dive into the murky waters of script loading
  3. github LABjs3.0源码
编辑 (opens new window)
#前端优化
上次更新: 2023/08/23, 09:32:05
前端国际化开发总结
yahoo前端优化35军规

← 前端国际化开发总结 yahoo前端优化35军规→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2025 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式