专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

2025 年的 import 和 export 也要支持 defer 了?

ins518 2025-05-15 18:21:30 技术文章 8 ℃ 0 评论

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1. 通用框架或者库的代码体积危机

前端开源框架或者库为了提升开发者的 DX 而采用的一种常见模式是使用单一入口文件来重新导出所有公共 API。然而,该方式会产生一个潜在的问题,即导致大量未使用的代码被包含在模块图谱 (Module Graph) 中。

Bash
// 统一入口文件 lodash.js
export {default as add} from "./add.js";
export {default as divide} from "./divide.js";
export {default as debounce} from "./debounce.js";
export {default as map} from "./map.js";

虽然可以使用称为 “tree-shaking” 的技术来解决此类问题,即跟踪模块导出的各个绑定的依赖关系,并移除那些未使用的重新导出。

Bash
// tree-shaking 会保证只导入并使用 add 和 multiply 函数
import {add, multiply} from './math';
console.log('Add:', add(2, 3));
// 输出: Add: 5
console.log('Multiply:', multiply(4, 5));
// 输出: Multiply: 20

然而,由于模块加载可能带来副作用,该技术并非总是可行,例如:不同的工具在 代码大小和正确性之间会做出权衡,从而导致 Web 应用程序优化不足,或者由于非纯模块 (non-pure module) 未按预期执行而导致难以调试。

// 非纯模块,其在加载时直接修改了全局状态
console.log('Non-pure module loaded!');
// 修改全局变量
window.someGlobalVariable = 'Modified by nonPureModule';
// 导出一个函数
export function doSomething() {
  console.log('Doing something...');
}

同时,当直接在浏览器中运行 ESM 时,tree-shaking 技术也存在不足,因为其默认需要全程序分析 (whole-program analysis)。而浏览器不会对整个程序进行静态分析 ,因为其只负责加载和执行模块,而不关心模块之间的依赖关系或未使用的代码。

2. 为什么需要延迟重新导出 (Deferred re-exports)

实际上,Web 应用通常包含大量 JavaScript 代码,从而对启动时间产生重大影响。一种可行的方法是加载尽可能少的必要代码,并预加载将来可能需要的代码。然而,该策略在实践中很难实现,常常导致 Web 应用程序优化不足。

导入延迟提案 (import defer proposal) 解决了部分问题,其允许以最小的代价延迟执行应用程序启动期间不需要的代码。例如:

// 该提案会加载./helpers.js 和其依赖,但是不会立即执行
import defer * as helpers from "./helpers.js";
function fn() {
  // helpers.js 仅在此时会执行
  helpers.doSomething();
}

延迟重新导出提案通过允许库将重新导出 (reexport) 标记为 “未使用则忽略” ,最终解决了通用前端框架或者库的体积危机问题。其实现了以下核心目的:

  • 遵循清晰的语义而非依赖工具定义的启发式方法
  • 原生 JS 平台也可以实现这些语义以避免加载未使用的代码
  • 与 import defer 提案集成,使重新导出 (reexport) 能够受益于相同的 “加载后,仅在实际需要时执行” 语义

延迟重新导出可以与 import defer 提案结合使用:

// 模块 math.js
export defer {add} from "./math/add.js";
export defer {sub} from "./math/sub.js";

当模块使用 import {add} from "./math.js"; 导入时,其会加载并执行./math.js 和 ./math/add.js,同时跳过 ./math/sub.js 及其所有依赖项。

3. 延迟重新导出模块的执行顺序

延迟重新导出的模块会在重新导出它们的模块之后执行,且按照重新导出的顺序执行,比如下面的示例:

// 重新导出模块 barrel.js
export defer {a} from "./a.js";
export {b} from "./b.js";
export defer {c} from "./c.js";
export {d} from "./d.js";
export defer {e} from "./e.js";
// 这里是入口导入文件 entrypoint.js
import {e, a, d} from "./barrel.js";

此时模块执行顺序为:

  • b.js
  • d.js
  • barrel.js
  • a.js
  • c.js
  • e.js
  • entrypoint.js

与按源代码顺序执行所需内容相比,始终在重新导出它们的模块之后执行延迟导出的模块 ,可以提高不同类型的模块图谱之间的一致性。

4. 延迟重新导出与 import defer 集成

import defer 提案规定,使用命名空间导入时,defer 关键字表示 “仅在实际需要时执行此模块”。对于模块命名空间对象,export defer 也遵循类似的语义:

// 模块 math.js
export defer {add} from "./math/add.js";
export defer {sub} from "./math/sub.js";
export {mul} from "./math/mul.js";
// 模块 index.js
import * as math from "./math.js";
// 所有方法都加载, 会执行 ./math.js 和 ./math/mul.js
math.add;
// 执行 ./math/add.js
math.sub;
// 执行 ./math/sub.js

在将 export defer 与 import defer 配对使用时,可以提供更多的控制:

// 模块 math2.js
export defer {add} from "./math/add.js";
export defer {sub} from "./math/sub.js";
// 模块 index2.js
import defer * as math from "./math2.js";
// 所有方法加载,但是任何方法都不会执行
math.add;
// 执行 ./math.js ./math/add.js
math.sub;
// 执行 ./math/sub.js

参考资料

https://github.com/tc39/proposal-deferred-reexports

https://github.com/tc39/proposal-defer-import-eval/

https://www.youtube.com/watch?v=0t-Le4kdaMg

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表