两个新的 JavaScript 提案,助你解决 ES 模块加载性能瓶颈!

大家好,我是 ConardLi


太长不看版(以后的文章我都会加入这个部分,基于 AI 总结,方便没空了解细节的同学快速阅读。)

本文介绍了两个 JavaScript 提案:Import Sync 提案和 Defer Import Eval 提案。Import Sync 提案建议引入一个同步导入函数 import.sync,用于在模块已加载的情况下同步导入模块。Defer Import Eval 提案建议引入一种新的同步导入形式 import defer,以避免在应用初始化过程中不必要的 CPU 工作,仅在访问模块属性时触发执行。

  1. Import Sync 提案

  • 引入 import.sync 函数,用于在模块已加载时同步导入模块。

  • 当模块不可同步获取或使用顶层 await 时,import.sync 会抛出错误。

  • 常见使用案例包括获取已加载的模块、条件加载和同步加载模块表达式和声明。

Defer Import Eval 提案

  • 引入 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 时,该函数会抛出一个新错误。模块是否可同步获取则是由主机决定的属性。

下面是一些常见的使用案例:

  1. 获取已加载的模块

  • 在网络环境中,如果模块已经加载,可以同步获取它。

import 'app';

    // 如果 'app' 模块已经加载,则这将始终工作
    const app = import.sync('app');
  1. 条件加载

  • 同步导入允许我们检查某模块或内置模块是否可用,比如检查 Node.js 内置模块 fs

let fs;
    try {
      fs = import.sync('node:fs');
    } catch {}

    if (fs) {
      // 只有当 fs 模块可用时,才使用 node:fs
    }
  1. 模块表达式和声明的同步加载

  • 同步导入模块表达式无需 TLA 和异步依赖:

// 立即日志输出 '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) {
  const module = await loadModuleAndDependencies(name);
  await executeAsyncSubgraphs(module);
  Object.defineProperty(object, name, {
    get: function() {
      delete object[name];
      const value = module.evaluateSync();
      Object.defineProperty(object, name, {
        value,
        writable: true,
        configurable: true,
        enumerable: true,
      });
      return value;
    },
    configurable: true,
    enumerable: true,
  });

  return object;
}

最后

这两个提案都是为了解决当前模块加载机制中的性能瓶颈,使我们可以更加灵活高效地管理模块的加载和评估。通过 Import Sync,我们能够在特定场景下高效利用同步导入的便利性,而延迟模块评估则进一步减轻了初始化时的 CPU 负担,使大规模应用的性能显著提升。

大家觉得这两个提案有用吗?欢迎在评论区留言。

抖音前端架构团队目前放出不少新的 HC ,有看起会的小伙伴可以看看这篇文章:抖音前端架构团队正在寻找人才!FE/Client/Server/QA,25 届校招同学可以直接用内推码:DRZUM5Z,或者加我微信联系。

提案地址:

  • https://github.com/tc39/proposal-import-sync

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

- END -

如果您关注前端+AI 相关领域可以扫码进群交流

 ee36c3954cdb3796ff55d7553bc14c57.jpeg

添加小编微信进群😊

关于奇舞团

奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

f1090849b36ed4293e7a86842a80b356.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值