导读:本期聚焦于小伙伴创作的《异步脚本加载时为什么会出现竞态条件,又该如何彻底解决?》,敬请观看详情。把两个带async属性的脚本先后写进页面,常会看到后请求的接口先返回、依赖对象未定义报错。这源于浏览器并行下载且不保证执行顺序的底层机制。若脚本A提供全局工具、脚本B调用它,网络波动就可能让B先跑。靠人为调整标签顺序无效,需用动态加载器按依赖链串行插入,或改为ES Module静态导入。下文给出可复用的加载控制器代码与对比分析,帮你在复杂页面里消除这类难以排查的随机故障。

在网页性能优化中,给script标签加上async属性可以让浏览器并行下载脚本而不阻塞解析。但多个异步脚本之间存在依赖关系时,执行顺序无法保证,这就产生了竞态条件。比如工具库先加载、业务脚本后加载,若业务脚本先执行就会因找不到全局函数而崩溃。

异步脚本加载时为什么会出现竞态条件,又该如何彻底解决?

一、异步脚本竞态条件的底层原理

浏览器在解析HTML时遇到带async的script标签,会立即发起网络请求,同时继续向下解析DOM。脚本下载完成后,浏览器暂停解析并执行该脚本,执行完毕再恢复解析。由于网络环境不稳定,先写的async脚本可能比后写的下载更慢,导致后写脚本先执行。

这种机制与传统同步脚本(不带async和defer)完全不同。同步脚本严格按照文档顺序执行,而async脚本的执行时机只取决于下载完成的时间点。当脚本A定义了window.Tool,脚本B使用window.Tool,若B先下载完并执行,就会抛出TypeError。这类错误具有随机性,在本地快网络难以复现,到了用户弱网环境频繁出现。

1.1 典型的出错代码布局

下面是一段容易引发竞态的页面代码。utils.js中挂载了全局对象,app.js依赖它,但两者都使用async。

<!DOCTYPE html>
<html>
<head>
  <script async src="utils.js"></script>
  <script async src="app.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

在utils.js中我们可能这样写:

window.Tool = {
  format: function (str) {
    return str.trim();
  }
};

而app.js直接调用:

var text = window.Tool.format('  hello  ');
document.getElementById('root').innerText = text;

当app.js先执行时,window.Tool还是undefined,代码在第1行就报错,后续逻辑全部失效。由于async不保证顺序,这个bug只在特定网络下出现,给排查带来很大困难。

二、常见错误应对及其局限

不少团队首先尝试通过调整标签前后位置来规避,认为把依赖库放在前面就更安全。但async属性让位置失去意义,浏览器依旧并行拉取,先写完不代表先执行。

另一种做法是去掉async改用defer。defer能保持执行顺序且并行下载,看似解决问题。但在动态插入脚本、微前端子应用、第三方埋点等场景,defer无法用于document.write或动态创建的标签,局限性明显。而且多个独立团队各自的defer脚本若隐式共享全局状态,仍可能在复杂组合下出错。

2.1 为什么单纯去掉async不够

如果页面由多个系统拼接,A系统注入async脚本,B系统也注入,你无法强迫对方改标签。此时需要一种不依赖对方配合的通用协调机制。下表对比了三种加载方式:

加载方式下载并行执行顺序适用场景
同步(无属性)严格文档序少量强依赖脚本
async不保证完全独立无依赖脚本
defer文档序静态已知依赖链

可以看到,当依赖关系动态且跨团队时,上述原生属性都难以单独胜任,必须引入加载控制器。

三、基于动态加载器的串行解决方案

我们可以编写一个loadScript函数,它返回Promise,在脚本onload后才resolve,从而用async/await控制顺序。这样无论网络多乱,依赖库一定先于业务脚本执行。

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    var s = document.createElement('script');
    s.src = src;
    // 不使用async,动态插入默认同步顺序可控
    s.onload = function () {
      resolve();
    };
    s.onerror = function () {
      reject(new Error('加载失败: ' + src));
    };
    document.head.appendChild(s);
  });
}

// 按顺序加载,彻底规避竞态
async function boot() {
  try {
    await loadScript('utils.js');
    await loadScript('app.js');
    console.log('所有脚本安全执行完毕');
  } catch (e) {
    console.error(e);
  }
}
boot();

这段代码中,loadScript创建了一个script元素并监听加载事件。由于我们没有给它加async,动态添加的脚本在插入DOM后按插入顺序执行,且await保证了utils.js的onload触发后才会插入app.js。即使utils.js网络很慢,app.js也会乖乖等着。

这种方案的优点是侵入性低,不用改原有脚本内容,只需统一走boot函数。缺点是失去了async带来的并行下载收益,但因为是有依赖的脚本,本来就不能并行执行,所以性能损失可以忽略。对于无依赖的独立脚本,仍可单独用原生async加载。

3.1 支持依赖图的增强版加载器

当脚本多且依赖复杂,可定义依赖配置,用递归加载。下面示例展示如何按deps字段串行加载前置项:

var manifest = {
  utils: { src: 'utils.js', deps: [] },
  api: { src: 'api.js', deps: ['utils'] },
  app: { src: 'app.js', deps: ['api', 'utils'] }
};

var cache = {};

function loadByKey(key) {
  if (cache[key]) return cache[key];
  var item = manifest[key];
  var p = Promise.resolve();
  item.deps.forEach(function (d) {
    p = p.then(function () { return loadByKey(d); });
  });
  p = p.then(function () { return loadScript(item.src); });
  cache[key] = p;
  return p;
}

loadByKey('app').then(function () {
  console.log('依赖树全部就绪');
});

利用cache避免重复加载,利用Promise链确保依赖顺序。这样在大型项目中,任何脚本只要声明deps,就能被安全调度,从根源消灭竞态条件。

四、使用ES Module从根本上规避

现代浏览器支持type="module"的脚本,其导入语法import会在执行前静态分析依赖,并且模块本身默认延迟执行且有序。把utils.js改为导出,app.js用import引入,浏览器自动处理顺序。

// utils.js
export function format(str) {
  return str.trim();
}

// app.js
import { format } from './utils.js';
var text = format('  hello  ');
document.getElementById('root').innerText = text;

在HTML中只需一行:

<script type="module" src="app.js"></script>

模块脚本天然解决竞态,因为依赖在图解析阶段完成,且模块只执行一次。缺点是旧浏览器需打包器降级,以及文件路径必须是URL形式。但新项目应优先采用此方案,既清晰又免维护加载器。

五、总结与实践建议

异步脚本竞态不是偶发玄学,而是async语义与依赖缺失顺序约束的必然冲突。在简单页面可用defer保序;跨团队动态注入请用Promise加载器串行控制;新项目直接上ES Module。无论哪种,核心原则都是让依赖项的执行先于使用者,而不是寄希望于网络巧合。

建议在代码评审中把async标签的使用列为检查项:凡是有全局变量共享或相互调用的脚本,禁止随意标async。通过架构层面的加载规范,可以让这类竞态条件在编码阶段就被消除,而不是等到线上报错才去抓包排查。

async_scriptrace_conditionscript_loading修改时间:2026-07-31 16:39:37

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。