大家好,我是
ConardLi
。
太长不看版(以后的文章我都会加入这个部分,基于 AI 总结,方便没空了解细节的同学快速阅读。)
:
本文介绍了两个 JavaScript 提案:Import Sync 提案和 Defer Import Eval 提案。Import Sync 提案建议引入一个同步导入函数
import.sync
,用于在模块已加载的情况下同步导入模块。Defer Import Eval 提案建议引入一种新的同步导入形式
import defer
,以避免在应用初始化过程中不必要的 CPU 工作,仅在访问模块属性时触发执行。
-
-
引入
import.sync
函数,用于在模块已加载时同步导入模块。
-
当模块不可同步获取或使用顶层 await 时,
import.sync
会抛出错误。
-
常见使用案例包括获取已加载的模块、条件加载和同步加载模块表达式和声明。
-
引入
import defer
语法,以避免应用初始化时不必要的 CPU 工作。
-
通过
import defer
导入的模块及其依赖项会被加载但不会立刻执行,只有在访问其属性时才触发执行。
-
-
使用
import defer
导入的模块,其异步依赖会立即执行,而同步部分会被推迟执行。
-
Import Sync 和 Defer Import Eval 提案旨在优化模块加载和执行,减少初始化期间的 CPU 占用。
-
通过同步导入和延迟执行,可以提高大型应用程序的性能,避免阻塞主事件循环。
-
提供了一个粗略的模块包装器实现示例,用于实现类似 Defer Import Eval 的行为,通过异步加载模块及其依赖项,并在访问属性时同步执行。
正文如下:
Import Sync 提案(Stage 1)
当前,在模块注册表(module registry)中已完全加载的模块情况下,有些需求场景中,我们可能会期望有一个同步导入(import)功能,以便在同步函数路径中加载动态说明符表达式。
原生 ESM 进入了成熟阶段,Node.js 引入了
require(esm)
特性,解锁了大量升级路径。尽管如此,从 CommonJS 迁移到 ES 模块的过程仍然存在很多问题,尤其是 CommonJS 用户习惯于在 Node.js 中同步动态导入模块。但是,现有的异步导入的方式通常需要我们修改代码路径来适应异步的行为。
本提案建议引入一个显式的同步导入函数
import.sync
,其语法和
Defer Import Eval
提案中定义的同步评估行为一致:
// 如果模块可同步获取,则同步导入模块
const ns = import.sync('./mod.js');
当模块不可同步获取或使用顶层 await 时,该函数会抛出一个新错误。模块是否可同步获取则是由主机决定的属性。
下面是一些常见的使用案例:
-
import 'app';
// 如果 'app' 模块已经加载,则这将始终工作
const app = import
.sync('app');
-
-
同步导入允许我们检查某模块或内置模块是否可用,比如检查 Node.js 内置模块
fs
:
let fs;
try {
fs = import.sync('node:fs');
} catch {}
if (fs) {
// 只有当 fs 模块可用时,才使用 node:fs
}
-
// 立即日志输出 'hello world'
import.sync(module {
console.log('hello world');
})
module dep {
console.log('hi');
}
module x {
import dep;
}
// 如果两个模块都是同步可用的,日志输出 'hi'
const instance = import.sync(x);
Defer Import Eval 提案(Stage 2.7)
大规模
JavaScript
应用不仅在加载时,甚至在执行初始化脚本时都会带来显著的性能开销。这通常在应用程序生命周期的后期才会显现,我们通常需要进行侵入性的代码优化来提高性能。
例如,在
Node.js CommonJS
模块系统中,通过代码重构来实现动态导入,可以避免不必要的执行:
// 优化前
const operation = require('operation');
exports.doSomething = function (target) {
return operation(target);
}
// 优化后
exports.doSomething = function (target) {
const operation = require('operation');
return operation(target);
}
对于 ES 模块,可以通过动态导入来实现懒加载:
export async function doSomething (target) {
const { operation } = await import('operations');
return operation(target);
}
然而,上面方法并没有解决性能瓶颈问题,尤其是初始化时的 CPU 占用。动态导入常需预加载步骤,并迫使所有函数及其调用者进入异步模型,这与程序的真实意图是不符合的,而且会导致 API 的更改。
本提案建议引入一个新的同步导入形式,通过该形式可避免在应用初始化过程中不必要的 CPU 工作,且无需改变模块 API 消费者的使用方式。当访问模块的属性时,仅在必要时触发同步评估。
// 示例语法
import defer * as yNamespace from "y";
这种导入方式会参与深层图加载,将模块及其依赖项加载至准备执行的状态,但不会立刻执行,只有在访问其属性时才触发执行。
例如:
const operation = import.defer('./operation');
function executeOperation(target) {
return operation.default(target);
}
我们还可以用它来实现同步检查模块或内置模块是否可用:
let fs;
try {
fs = import.defer('node:fs');
} catch {}
if (fs) {
// 使用 node:fs,仅在其可用时
}
同步属性访问必须同步进行,因此不可能将使用顶级
await
的模块推迟执行。使用
import defer
语法导入的模块,其异步依赖和传递依赖将会被立即执行,而只有同步部分会被推迟。
可能大家会有个问题,为什么需优化执行?加载不是瓶颈吗?
虽然加载时间确实是网页上的主要瓶颈,但大型应用程序也存在初始化期间阻塞 CPU 的问题。一些应用程序在初始化主应用图时会占用 100 毫秒的 CPU 时间。加载时间可以达到多秒级,但从初始化期间释放主事件循环也是至关重要的。
当前我们可以通过模块包装器实现类似行为,下面一个粗略的实现示例:
// LazyModuleLoader.js
async function loadModuleAndDependencies(name) {
const loadedModule = await import.load(`./${name}.js`); // 加载模块,需等待异步完成
const parsedModule = loadedModule.parse();
await Promise.all(parsedModule.imports.map(loadModuleAndDependencies)); // 加载所有依赖
return parsedModule;
}
async function executeAsyncSubgraphs(module) {
if (module.hasTLA) return module.evaluate();
return Promise.all(module.importedModules.map(executeAsyncSubgraphs));
}
export default async function lazyModule(object, name