1. 通用框架或者库的代码体积危机
前端开源框架或者库为了提升开发者的 DX 而采用的一种常见模式是使用单一入口文件来重新导出所有公共 API。然而,该方式会产生一个潜在的问题,即导致大量未使用的代码被包含在模块图谱 (Module Graph) 中。
1 | // 统一入口文件 lodash.js |
虽然可以使用称为 “tree-shaking” 的技术来解决此类问题,即跟踪模块导出的各个绑定的依赖关系,并移除那些未使用的重新导出。
1 | // tree-shaking 会保证只导入并使用 add 和 multiply 函数 |
然而,由于模块加载可能带来副作用,该技术并非总是可行,例如:不同的工具在 代码大小和正确性之间会做出权衡,从而导致 Web 应用程序优化不足,或者由于非纯模块 (non-pure module) 未按预期执行而导致难以调试。
1 | // 非纯模块,其在加载时直接修改了全局状态 |
2. 为什么需要延迟重新导出 (Deferred re-exports)
实际上,Web 应用通常包含大量 JavaScript 代码,从而对启动时间产生重大影响。一种可行的方法是加载尽可能少的必要代码,并预加载将来可能需要的代码。然而,该策略在实践中很难实现,常常导致 Web 应用程序优化不足。
导入延迟提案 (import defer proposal) 解决了部分问题,其允许以最小的代价延迟执行应用程序启动期间不需要的代码。例如:
1 | // 该提案会加载./helpers.js 和其依赖,但是不会立即执行 |
延迟重新导出提案通过允许库将重新导出 (reexport) 标记为 “未使用则忽略” ,最终解决了通用前端框架或者库的体积危机问题。其实现了以下核心目的:
- 遵循清晰的语义而非依赖工具定义的启发式方法
- 原生 JS 平台也可以实现这些语义以避免加载未使用的代码
- 与 import defer 提案集成,使重新导出 (reexport) 能够受益于相同的 “加载后,仅在实际需要时执行” 语义
延迟重新导出可以与 import defer 提案结合使用:
1 | // 模块 math.js |
当模块使用 import {add} from “./math.js”; 导入时,其会加载并执行./math.js 和 ./math/add.js,同时跳过 ./math/sub.js 及其所有依赖项。
3. 延迟重新导出模块的执行顺序
延迟重新导出的模块会在重新导出它们的模块之后执行,且按照重新导出的顺序执行,比如下面的示例:
1 | // 重新导出模块 barrel.js |
1 | // 这里是入口导入文件 entrypoint.js |
此时模块执行顺序为:
- b.js
- d.js
- barrel.js
- a.js
- c.js
- e.js
- entrypoint.js
与按源代码顺序执行所需内容相比,始终在重新导出它们的模块之后执行延迟导出的模块 ,可以提高不同类型的模块图谱之间的一致性。
4. 延迟重新导出与 import defer 集成
import defer 提案规定,使用命名空间导入时,defer 关键字表示 “仅在实际需要时执行此模块”。对于模块命名空间对象,export defer 也遵循类似的语义:
1 | // 模块 math.js |
1 | // 模块 index.js |
在将 export defer 与 import defer 配对使用时,可以提供更多的控制:
1 | // 模块 math2.js |
1 | // 模块 index2.js |