For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-deferred-reexports.md.
  • 简体中文
  • Deferred Re-exports S2

    中文标题:延迟再导出

    提案概览
    提案速览

    该提案旨在允许库使用桶再导出而不加载未使用的代码,通过引入 export defer 语法,将再导出标记为“未使用则忽略”。它定义了跳过未使用模块的清晰语义,支持原生实现,并与现有的 import defer 提案集成。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    延迟再导出

    状态

    提案发起人:Nicolò RibaudoCaio Lima

    作者:Nicolò Ribaudo

    阶段:2

    介绍

    Web 应用通常包含大量 JavaScript 代码,这会对启动时间产生显著影响。减少这种影响的一种方法是仔细地按需加载尽可能少的代码,并可能预取未来可能需要的代码。然而,在实践中这已被证明是困难的,常常导致 Web 应用优化不足。

    import defer 提案 解决了该问题的一部分,通过最小程度地阻碍来推迟执行应用启动时不需要的代码。它引入了语法来将 import 声明标记为“可延迟直到其导出被访问”:

    // 此声明 _加载_ helpers.js 及其依赖,
    // 但尚未执行它。
    import defer * as helpers from "./helpers.js";
    
    function fn() {
      // helpers.js 仅在此处执行。
      helpers.doSomething();
    }

    作为该提案的一部分,我们考虑过(2023-11 幻灯片2024-04 export ... from 幻灯片)为 export ... from 声明添加类似功能。然而,export ... from 声明有机会跳过 加载 未使用的模块,因此当我们将 import defer 推进到第 2.7 阶段时,决定(2024-04 import defer 幻灯片)将等价的 export ... from 特性保留为单独的提案。

    问题陈述

    库为提升其使用者的开发者体验而采用的常见模式是拥有一个单一的入口点,该入口点再导出所有公共 API。然而,这存在问题:它导致大量未使用的代码被包含在模块图中。

    一些工具使用称为“摇树优化”的技术来解决这个问题:他们追踪由模块导出的各个绑定的依赖关系,并移除仅用于未使用导出的传递导入(或再导出)。然而,由于模块被加载可能产生副作用,此操作并不总是可行:不同工具在代码大小和正确性之间做出不同的权衡,常常导致 Web 应用优化不足或由于非纯模块未被按预期执行而导致的难以调试的问题。此外,当直接在浏览器中运行 ESM 时,该技术是_不可能_实现的,因为它需要整个程序的分析。

    提案

    本提案旨在允许库继续使用从单一入口点提供多个独立功能的流行模式,同时消除其带来的缺点。

    它通过允许库将再导出标记为“未使用则忽略”来实现这一目标,定义:

    1. 清晰的可遵循语义,而不是依赖工具定义的启发式规则;
    2. 原生 JS 平台也可以实现的语义,以避免加载未使用的代码;
    3. import defer 提案的集成,以允许这些再导出受益于相同的“加载后,仅在确实需要时执行”的语义。

    本提案使用下面的语法,与 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 及其所有依赖。

    执行顺序

    延迟再导出在再导出它们的模块_之后_执行(如果它们被执行),按照它们被再导出的顺序。给定以下示例:

    // 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.jsd.jsbarrel.jsa.jse.jsentrypoint.js

    始终在再导出它们的模块之后执行 defer-exported 模块,与按源顺序执行所需模块相比,可以在不同类型的模块图中实现更大的一致性。

    比较示例 所有示例都导入如上定义的 `./barrel.js` 文件。
    标题模块最后执行 export defer交错执行 export defer
    未导入延迟绑定
    // entry.js
    import { b } from "./barrel.js"
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./entry.js

  • ./b.js

  • ./d.js

  • ./barrel.js

  • ./entry.js

  • 导入第一个延迟绑定
    // entry.js
    import { a } from "./barrel.js"
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./a.js

    • ./entry.js

  • ./a.js

  • ./b.js

  • ./d.js

  • ./barrel.js

  • ./entry.js

  • 导入中间延迟绑定
    // entry.js
    import { c } from "./barrel.js"
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./c.js

    • ./entry.js

  • ./b.js

  • ./c.js

  • ./d.js

  • ./barrel.js

  • ./entry.js

  • 导入两个延迟绑定
    // entry.js
    import { a, c } from "./barrel.js"
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./a.js

    • ./c.js

    • ./entry.js

  • ./a.js

  • ./b.js

  • ./c.js

  • ./d.js

  • ./barrel.js

  • ./entry.js

  • 由两个不同文件导入两个延迟绑定
    // entry.js
    import "./sub-1.js";
    import "./sub-2.js";
    // sub-1.js
    import { a } from "./barrel.js";
    // sub-2.js
    import { c } from "./barrel.js";
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./a.js

    • ./sub-1.js

    • ./c.js

    • ./sub-2.js

    • ./entry.js

  • ./a.js

  • ./b.js

  • ./d.js

  • ./barrel.js

  • ./sub-1.js

  • ./c.js

  • ./sub-2.js

  • ./entry.js

  • 由两个不同文件导入两个延迟绑定(相反顺序)
    // entry.js
    import "./sub-2.js";
    import "./sub-1.js";
    // sub-2.js
    import { c } from "./barrel.js";
    // sub-1.js
    import { a } from "./barrel.js";
    • ./b.js

    • ./d.js

    • ./barrel.js

    • ./c.js

    • ./sub-2.js

    • ./a.js

    • ./sub-1.js

    • ./entry.js

  • ./b.js

  • ./c.js

  • ./d.js

  • ./barrel.js

  • ./sub-2.js

  • ./a.js

  • ./sub-1.js

  • ./entry.js

  • 所提议的顺序在工具中 polyfill 也简单得多,工具可以从导入的文件中提取 export defer 声明,并在导入者模块中用 import 语句替换它们。

    不支持且不计划支持 export defer * from './foo.js',因为我们需要显式地知道延迟再导出的名称,而不需要进一步加载依赖,这是延迟网络加载的要求。

    命名空间导入

    在模块命名空间对象上,export defer 将“去优化”并急切地加载所有模块,但仍然允许延迟执行这些模块:

    // math.js
    export defer { add } from "./math/add.js";
    export defer { sub } from "./math/sub.js";
    export { mul } from "./math/mul.js";
    console.log('executed math');
    // index.js
    import * as math from "./math.js"; // 加载所有内容(包括 ./math.js 和 ./math/mul.js),记录 'executed math'
    
    math.add; // 执行 ./math/add.js
    math.sub; // 执行 ./math/sub.js

    类似的去优化也会发生在与 export * from 交互时,该操作将导致除了 default 以外的导出所需的所有可选依赖被急切加载:

    // math.js
    export defer { add as default } from "./math/add.js";
    export defer { sub } from "./math/sub.js";
    export { mul } from "./math/mul.js";
    // index.js
    export * from "./math.js"; // 加载(./math.js、./math/mul.js 和 ./math/sub.js),不加载 ./math/add.js

    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";
    console.log('executed math');
    // index.js
    import * as math from "./math.js"; // 加载所有内容(包括 ./math.js 和 ./math/mul.js),不执行任何内容
    
    math.add; // 执行 ./math/add.js,记录 'executed math'
    math.sub; // 执行 ./math/sub.js

    所有依赖仍然被提前加载,并且使用顶层 await 的延迟模块会被预执行(就像单独使用 import defer 提案时的情况),以便在请求时同步可用。

    package.json#exports 的比较

    最初由 Node.js 引入,但现在被多个构建工具认可,package.json#exports 允许定义 JavaScript 包的入口点(文档)。

    一个库,在此示例中名为 react,可以有一个与环境无关的主入口点,然后为专门的功能有多个入口点:

    {
      "exports": {
        ".": "./lib/index.js",
        "./dom": "./lib/dom.js",
        "./ssr": "./lib/ssr.js"
      }
    }

    导入时,用户可以使用“好看”的入口点名称,而不必导入库的内部文件:

    import { createElement } from "react"; // 导入 react/lib/index.js
    import { render } from "react/dom"; // 导入 react/lib/dom.js

    这允许库为不同的功能提供单独的入口点,而无需用户使用库选择的文件结构中的深层路径。它为库的用户提供了类似的体验,就好像用户打算导入的所有文件都位于该库的根文件夹中一样。package.json#exports 完全在解析时工作。

    export deferpackage.json#exports 部分重叠:两者都允许导入者导入库的_部分_而不必编写多个复杂路径。然而,它们有不同的权衡,不可互换,但可以协同工作:

    • package.json#exports 仅在“Node.js 包”级别工作。复杂的应用通常有内部文件夹,这些文件夹在概念上是内部库,有自己的入口点。export defer 作为一种语言特性,不受 Node.js "包"概念的约束,可以在应用内的任何地方使用。
    • export defer 将原生在浏览器中工作,而 package.json#exports 是 Node.js 解析方式特有的功能。
    • package.json#exports 允许定义库中哪些 JavaScript 文件是内部的,哪些是公共 API 的一部分,防止库的用户导入内部文件。export defer 与此用例无关。

    已有流行的库混合使用 package.json#exports 和桶文件。一个例子是 msw("Mock Service Worker"),它:

    • 使用 package.json#exports 定义特定环境的入口点(例如 msw/nodemsw/browser)— msw/package.json
    • 在每个入口点,它使用 export { ... } from 再导出多个工具 — msw/src/core/index.ts