Rspack 源码解析(三):从入口到依赖图,读懂 Make 阶段的 Rust 任务循环

本篇是 Rspack 源码解析系列第三篇。前两篇我们已经从 rspack build 走到了 Rust 侧的 22-Pass 编译流水线;这一篇开始进入第一个真正干活的 Pass:Build Module Graph。目标很明确:搞清楚一个入口文件是怎样被解析、构建,并最终长成一张依赖图的。

前言

上一篇最后留下了一个问题:run_passes() 的第一个 Pass 是 BuildModuleGraphPhasePass,那它究竟做了什么?

如果只从结果看,Rspack 读到了 src/index.ts,发现其中有:

ts 复制代码
import { sum } from './math';
import('./lazy');

console.log(sum(1, 2));

最后构建出一张类似下面的图:

text 复制代码
EntryDependency("./src/index.ts")
              |
              v
       NormalModule(index.ts)
          |              |
          |              +-- import() --> NormalModule(lazy.ts)
          v
   NormalModule(math.ts)

不过,真实实现并不是一个简单的递归 build(index) -> build(math)。Rspack 需要处理 Loader、不同模块类型、插件钩子、循环依赖、错误诊断,以及 watch 模式下的增量重建。因此它把这个过程拆成一组任务:耗时的解析、读取和构建在后台执行;对共享模块图的修改集中回到主任务队列执行。

这也是这一篇最值得学习的 Rust 设计。

先建立四个概念

在跟代码之前,先把几个极容易混淆的概念分开。它们在 Rspack 中并不是同一个东西。

概念 可以把它理解成 典型例子
Dependency 一次"我要引用谁"的声明 import './math'
Module 这个请求最终对应的模块实体 NormalModule(math.ts)
ModuleGraphConnection 一条已经解析完成的边 index.ts --import--> math.ts
ModuleGraph 保存模块、依赖和边的总图 整个项目的依赖关系

注意:Dependency 不等于 Module

同一个 math.ts 可以被多个文件导入,因此会有多个 Dependency,但通常只需要一个 Module。这也是后面 AddTask 要检查模块是否已经存在的原因。

此外,模块图还记录依赖的父级、模块的 issuer(引入者)、异步依赖块等信息。后面的 Tree Shaking、代码分割和生成 Chunk Graph 都要以它为输入,所以 Make 阶段并不是"读文件"这么简单,而是在构建一份供后续 Pass 消费的核心中间结果。

从 22 个 Pass 进入 Make

先回到上一篇结束的位置。Compiler::compile() 会调用 Compilation::run_passes()

rust 复制代码
// crates/rspack_core/src/compilation/run_passes.rs
pub async fn run_passes(
  &mut self,
  _plugin_driver: SharedPluginDriver,
  cache: &mut dyn Cache,
) -> Result<()> {
  let passes: Vec<Box<dyn PassExt>> = vec![
    Box::new(BuildModuleGraphPhasePass),
    Box::new(FinishModulesPhasePass),
    Box::new(SealPass),
    // 后面还有优化、代码生成、产物生成等 Pass
  ];

  for pass in &passes {
    pass.run(self, cache).await?;
  }
  Ok(())
}

这里可以顺便观察一个很实用的 Rust 写法:Vec<Box<dyn PassExt>>

  • PassExt 是所有 Pass 共同实现的 trait;
  • 不同 Pass 的具体类型不同,不能直接放进同一个 Vec
  • Box<dyn PassExt> 通过 trait object 把它们统一成"可以运行的 Pass";
  • await? 表示等待当前 Pass 完成,如果失败则立刻把 Result 中的错误向上传播。

PassExt::run() 并不直接执行业务逻辑,它先统一处理是否启用、日志计时和缓存钩子,再调用各 Pass 自己的实现:

rust 复制代码
// crates/rspack_core/src/compilation/pass.rs(简化)
async fn run(&self, compilation: &mut Compilation, cache: &mut dyn Cache) -> Result<()> {
  if !self.is_enabled(compilation) {
    return Ok(());
  }

  self.before_pass(compilation, cache).await;
  let result = self.run_pass_with_cache(compilation, cache).await;
  if result.is_ok() {
    self.after_pass(compilation, cache).await;
  }
  result
}

这是一种很值得借鉴的设计:把每个阶段共有的"框架逻辑"收敛在 trait 的默认方法中,让具体 Pass 只关心自己的工作。

Build Module Graph 不是一个步骤,而是四个子阶段

第一个 Pass 的实现位于 crates/rspack_core/src/compilation/build_module_graph/pass.rs

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/pass.rs(简化)
async fn run_pass_with_cache(
  &self,
  compilation: &mut Compilation,
  _cache: &mut dyn Cache,
) -> Result<()> {
  let plugin_driver = compilation.plugin_driver.clone();

  make_hook_pass(compilation, plugin_driver.clone()).await?;
  build_module_graph_pass(compilation).await?;
  finish_make_pass(compilation, plugin_driver.clone()).await?;
  finish_module_graph_pass(compilation).await?;

  Ok(())
}

因此完整顺序是:

text 复制代码
make hook
  -> build module graph
  -> finish make hook
  -> finish module graph

这里有一个细节很重要:make 并不等于"构建模块图"本身。

  • make hook:插件把入口依赖注册到 Compilation
  • build module graph:由入口开始真正解析、构建并连接所有模块;
  • finish make hook:插件还有最后一次补充 include 依赖的机会;
  • finish module graph:再做一次整理,移除已经不可达的模块。

把插件扩展点和核心算法拆开,Rspack 才能在保持 webpack 兼容性的同时,让 Rust 内部的构图过程有清晰边界。

第一步:入口插件把入口变成 Dependency

入口不是 Compilation 天生就有的模块,而是 EntryPluginmake hook 中添加进去的。

rust 复制代码
// crates/rspack_plugin_entry/src/lib.rs(简化)
#[plugin_hook(CompilerCompilation for EntryPlugin)]
async fn compilation(
  &self,
  compilation: &mut Compilation,
  params: &mut CompilationParams,
) -> Result<()> {
  compilation.set_dependency_factory(
    DependencyType::Entry,
    params.normal_module_factory.clone(),
  );
  Ok(())
}

#[plugin_hook(CompilerMake for EntryPlugin)]
async fn make(&self, compilation: &mut Compilation) -> Result<()> {
  compilation
    .add_entry(self.inner.dependency.clone(), self.inner.options.clone())
    .await
}

上面的代码完成了两件事:

  1. DependencyType::Entry 指定 NormalModuleFactory。之后入口依赖需要被解析时,Rspack 就知道该找哪个工厂;
  2. make 阶段调用 add_entry(),把 EntryDependency 放入模块图和 Compilation.entries

EntryPlugin 中的依赖使用了 LazyLock<BoxDependency>

rust 复制代码
// crates/rspack_plugin_entry/src/lib.rs(简化)
dependency: LazyLock<BoxDependency, Box<dyn FnOnce() -> BoxDependency + Send>>,

它的作用不只是"延迟创建"。源码注释说明,这个入口依赖需要在多次构建时保持稳定,才能让增量构建判断它没有变化并复用已有结果。一个很小的类型选择,背后其实服务于 watch 模式的性能。

Compilation::add_entry() 先把依赖加入模块图,再按 entry name 写入 entries

rust 复制代码
// crates/rspack_core/src/compilation/mod.rs(简化)
pub async fn add_entry(&mut self, entry: BoxDependency, options: EntryOptions) -> Result<()> {
  let entry_id = *entry.id();
  let entry_name = options.name.clone();

  self
    .build_module_graph_artifact
    .get_module_graph_mut()
    .add_dependency(entry);

  if let Some(name) = &entry_name {
    self.entries.entry(name.to_owned()).or_default()
      .dependencies.push(entry_id);
  } else {
    self.global_entry.dependencies.push(entry_id);
  }

  self.plugin_driver.compilation_hooks.add_entry.call(self, entry_name.as_deref()).await?;
  Ok(())
}

此时入口只是一个还没解析的 DependencyId,而不是 NormalModule。真正从请求字符串找到文件并创建模块的工作,下一阶段才开始。

第二步:切除旧结果,找出需要重建的依赖

build_module_graph_pass() 会进入 build_module_graph()

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/mod.rs(简化)
pub async fn build_module_graph(
  compilation: &Compilation,
  mut artifact: BuildModuleGraphArtifact,
  exports_info_artifact: ExportsInfoArtifact,
) -> Result<(BuildModuleGraphArtifact, ExportsInfoArtifact)> {
  let mut params = Vec::new();

  params.push(UpdateParam::BuildEntry(entry_dependency_ids));
  params.push(UpdateParam::CheckNeedBuild);

  if !compilation.modified_files.is_empty() {
    params.push(UpdateParam::ModifiedFiles(compilation.modified_files.clone()));
  }
  if !compilation.removed_files.is_empty() {
    params.push(UpdateParam::RemovedFiles(compilation.removed_files.clone()));
  }

  artifact.reset_temporary_data();
  update_module_graph(compilation, artifact, exports_info_artifact, params).await
}

这里可以看出 Rspack 没有把"首次构建"和"增量构建"做成两套流程,而是把变化描述成 UpdateParam

  • BuildEntry:确保入口被构建;
  • CheckNeedBuild:处理不可缓存的模块;
  • ModifiedFiles:处理受文件变更影响的模块和依赖;
  • RemovedFiles:处理文件被删除后的失效关系。

接着 update_module_graph() 使用 Cutout 先从旧图中切出受影响区域,得到需要重新构建的 (DependencyId, Option<ModuleIdentifier>) 集合,然后交给 repair() 修复:

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/mod.rs(简化)
pub async fn update_module_graph(...) -> Result<(BuildModuleGraphArtifact, ExportsInfoArtifact)> {
  artifact.state = BuildModuleGraphArtifactState::Initialized;
  let mut cutout = Cutout::default();

  let build_dependencies = cutout.cutout_artifact(compilation, &mut artifact, params);
  let revoked_modules = artifact.revoked_modules().copied().collect();
  compilation.plugin_driver.compilation_hooks.revoked_modules
    .call(compilation, &revoked_modules).await?;

  let (mut artifact, exports_info_artifact) = repair(
    compilation,
    artifact,
    exports_info_artifact,
    build_dependencies,
  ).await?;

  cutout.fix_artifact(&mut artifact);
  Ok((artifact, exports_info_artifact))
}

可以把它理解成编辑一张已经存在的图:先擦掉受影响的枝干,再补上新枝干,最后修正 issuer、失败模块的 build meta 等关联信息。这样做是 Rspack 支持快速重编译的基础,也解释了为什么这里的模块图状态不直接全部挂在 Compilation 普通字段里,而是放进 BuildModuleGraphArtifact 这个阶段产物中。

首次 build 时旧图为空,效果就等同于"从入口开始新建整张图"。

第三步:任务循环驱动构图

repair() 先按照父模块对待构建依赖分组:入口依赖没有父模块,普通依赖则有来源模块。无论从哪里开始,最终都会生成 FactorizeTask

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/mod.rs(简化)
let init_tasks = grouped_deps.into_iter().flat_map(|(parent, dependencies)| {
  if let Some(module_identifier) = parent {
    vec![Box::new(ProcessDependenciesTask {
      original_module_identifier: module_identifier,
      dependencies,
      from_unlazy: false,
    }) as Box<dyn Task<TaskContext>>]
  } else {
    dependencies.into_iter().map(|dep_id| {
      Box::new(FactorizeTask { dependencies: vec![dependency], .. })
        as Box<dyn Task<TaskContext>>
    }).collect()
  }
}).collect();

let mut ctx = TaskContext::new(compilation, artifact, exports_info_artifact);
run_task_loop(&mut ctx, init_tasks).await?;

TaskContext 很值得注意。它将构图时需要的数据集中起来:编译配置、resolver、文件系统、插件驱动、模块工厂和当前 artifact。每一个 task 不需要持有整个 Compilation 的可变引用,这就避免了异步并发时最常见的借用冲突。

任务循环在 crates/rspack_core/src/utils/task_loop.rs

rust 复制代码
// crates/rspack_core/src/utils/task_loop.rs(简化)
pub enum TaskType {
  Main,
  Background,
}

pub trait Task<Ctx>: Debug + Send + Any + AsAny {
  fn get_task_type(&self) -> TaskType;
  async fn main_run(self: Box<Self>, context: &mut Ctx) -> TaskResult<Ctx>;
  async fn background_run(self: Box<Self>) -> TaskResult<Ctx>;
}

整个图构建过程可以记为下面这个循环:

text 复制代码
FactorizeTask (Background)
  -> FactorizeResultTask (Main)
  -> AddTask (Main)
  -> BuildTask (Background)
  -> BuildResultTask (Main)
  -> ProcessDependenciesTask (Main)
  -> 为子依赖创建更多 FactorizeTask

这里的职责分配很清楚:

  • Background task:模块解析、Loader 执行、读取文件、语法分析等耗时操作;
  • Main task :更新 ModuleGraph、去重模块、连接依赖边等共享状态操作。

这并不表示所有代码只运行在一个"主线程"。这里的 Main 更准确地说是任务循环中串行处理的队列;Background task 会通过 Tokio 任务运行,并把结果经 channel 发回。串行写图降低了并发修改复杂图结构的成本,同时保留了解析和构建的并行度。

第四步:Factorize,把请求变成模块

FactorizeTask 是"模块工厂化"的阶段。它从 Dependency 提取 request、context、issuer 和 resolve options,然后调用对应的 ModuleFactory

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/factorize.rs(简化)
async fn background_run(mut self: Box<Self>) -> TaskResult<TaskContext> {
  let dependency = &self.dependencies[0];
  let request = dependency
    .as_module_dependency()
    .map(|d| d.request().to_string())
    .unwrap_or_default();

  let mut create_data = ModuleFactoryCreateData {
    context,
    request,
    dependencies: self.dependencies,
    issuer: self.issuer,
    issuer_identifier: self.original_module_identifier,
    resolve_options: self.resolve_options,
    // 还会收集 file/missing/context dependencies 和 diagnostics
    ..
  };

  let factory_result = self.module_factory.create(&mut create_data).await?;
  Ok(vec![Box::new(FactorizeResultTask { factory_result, .. })])
}

factorize 这个词可以理解成"把抽象请求具体化":例如将 ./math 配合 issuer src/index.tsresolve.extensions、alias 等配置,解析成具体资源,并创建对应类型的模块。

对于一般的 JavaScript/TypeScript 文件,使用的是 NormalModuleFactory。它的 create() 先触发 before_resolve,再执行 factorize,最后触发 after_factorize

rust 复制代码
// crates/rspack_core/src/normal_module_factory.rs(简化)
async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result<ModuleFactoryResult> {
  if let Some(result) = self.before_resolve(data).await? {
    return Ok(result);
  }

  let mut result = self.factorize(data).await?;
  if let Some(module) = &mut result.module {
    self.plugin_driver.normal_module_factory_hooks
      .after_factorize.call(data, module).await?;
  }
  Ok(result)
}

这层抽象的意义是:调用方只需要处理"某类依赖要一个模块",不用把 Normal Module、Context Module、External Module 等分支写死在构图循环中。不同依赖类型通过 dependency_factories 找到各自的 ModuleFactory

如果解析失败且没有开启 bail,Rspack 不会立刻让整个构建崩掉。FactorizeTask 会把错误转换为 Diagnostic,继续完成能完成的构图,最终再统一报告。这也是打包工具比普通脚本更重视错误收集而非"第一个错误立即退出"的原因。

第五步:AddTask,把 Module 和边写入图中

后台解析完成后,FactorizeResultTask 产生 AddTask。它在串行主队列中处理两个很关键的事情:模块去重和依赖连边。

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/add.rs(简化)
async fn main_run(self: Box<Self>, context: &mut TaskContext) -> TaskResult<TaskContext> {
  let module_identifier = self.module.identifier();
  let module_graph = &mut context.artifact.module_graph;

  if module_graph
    .module_graph_module_by_identifier(&module_identifier)
    .is_some()
  {
    set_resolved_module(
      module_graph,
      self.original_module_identifier,
      self.dependencies,
      module_identifier,
    )?;
    return Ok(vec![]);
  }

  module_graph.add_module_graph_module(*self.module_graph_module);
  context.exports_info_artifact.new_exports_info(module_identifier);
  set_resolved_module(
    module_graph,
    self.original_module_identifier,
    self.dependencies,
    module_identifier,
  )?;

  Ok(vec![Box::new(BuildTask { module: self.module, .. })])
}

这里先检查 ModuleIdentifier 是否已经存在。

例如 index.tsother.ts 都导入 ./math 时,两个依赖都会分别经历 factorize,但写图时发现 math.ts 的模块已经存在,Rspack 只需要补上新的 connection,不应重复构建一份 math.ts

set_resolved_module() 是这张图真正"长边"的地方:

rust 复制代码
fn set_resolved_module(
  module_graph: &mut ModuleGraph,
  original_module_identifier: Option<ModuleIdentifier>,
  dependencies: Vec<BoxDependency>,
  module_identifier: ModuleIdentifier,
) -> Result<()> {
  for dependency in dependencies {
    module_graph.set_resolved_module(
      original_module_identifier,
      *dependency.id(),
      module_identifier,
    )?;
    module_graph.add_dependency(dependency);
  }
  Ok(())
}

因此,一条边需要三个信息:来源模块 original_module_identifier、边本身的 DependencyId、目标模块 module_identifier。入口没有来源模块,所以它的 original_module_identifierNone

第六步:BuildTask,Loader 和 Parser 在这里发生

新模块第一次加入图后才需要进入 BuildTask。这是一个 Background task:

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/build.rs(简化)
async fn background_run(self: Box<Self>) -> TaskResult<TaskContext> {
  let mut module = self.module;

  self.plugin_driver.compilation_hooks.build_module
    .call(self.compiler_id, self.compilation_id, &mut module).await?;

  let result = module.build(
    BuildContext {
      compiler_options: self.compiler_options,
      resolver_factory: self.resolver_factory,
      plugin_driver: self.plugin_driver.clone(),
      fs: self.fs,
      // ...
    },
    None,
  ).await?;

  Ok(vec![Box::new(BuildResultTask { build_result: Box::new(result), .. })])
}

NormalModule 来说,build() 中的核心顺序是:

text 复制代码
before_loaders hook
  -> run_loaders(...)
  -> 生成 Source
  -> Parser 分析源码
  -> 得到 dependencies 与 AsyncDependenciesBlock

其中 Loader 先把源文件转成适合模块类型的内容,例如 TypeScript 可能经 Loader 处理为 JavaScript;随后 parser 才从处理后的源码中找出 importrequireimport() 等依赖。BuildResult 正是把这个结果带回主队列的载体:

rust 复制代码
// crates/rspack_core/src/module.rs
pub struct BuildResult {
  pub module: BoxModule,
  pub dependencies: Vec<BoxDependency>,
  pub blocks: Vec<Box<AsyncDependenciesBlock>>,
  pub optimization_bailouts: Vec<String>,
}

这里的 BoxModule 和前面的 BoxDependency 同理,都是为了在运行时统一承载不同具体类型的 trait object。Rust 的静态类型并没有丢失,只是封装在 Box<dyn Module> 后面,通过 trait 方法统一调用。

第七步:收集子依赖,再把循环推下去

BuildResultTask 在主队列中接收构建结果,记录文件依赖、错误和优化信息,然后把模块解析出的依赖写回 ModuleGraph

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/build.rs(简化)
for dependency in build_result.dependencies {
  let dependency_id = *dependency.id();
  module.add_dependency_id(dependency_id);
  module_graph.set_parents(
    dependency_id,
    DependencyParents {
      module: module.identifier(),
      block: None,
      index_in_block,
    },
  );
  module_graph.add_dependency(dependency);
}

module_graph.add_module(module);

Ok(vec![Box::new(ProcessDependenciesTask {
  dependencies: dependency_ids,
  original_module_identifier: module_identifier,
  from_unlazy: false,
})])

AsyncDependenciesBlock 也会在此处被遍历和登记。它代表动态导入等异步边,后续 BuildChunkGraphPass 会根据这些 block 把同步模块和异步模块组织成不同的 Chunk。我们先记住:Make 阶段负责识别并记录异步边,真正分 chunk 是后面的事。

接下来是 ProcessDependenciesTask。它不会自己解析文件,而是按 resource_identifier 将等价依赖合并,然后为每一组创建新的 FactorizeTask

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/graph_updater/repair/process_dependencies.rs(简化)
async fn main_run(self: Box<Self>, context: &mut TaskContext) -> TaskResult<TaskContext> {
  let mut sorted_dependencies = HashMap::default();

  for dependency_id in dependencies {
    let dependency = module_graph.dependency_by_id(&dependency_id);
    let key = dependency.resource_identifier();
    sorted_dependencies.entry(key).or_default().push(dependency.clone());
  }

  for dependencies in sorted_dependencies.into_values() {
    tasks.push(Box::new(FactorizeTask {
      original_module_identifier: Some(module.identifier()),
      dependencies,
      module_factory,
      // ...
    }));
  }
  Ok(tasks)
}

至此又回到了 Factorize。只要新模块还能解析出新的依赖,任务循环就会持续产生下一批任务;直到没有任务可执行,整张从入口可达的模块图才构建完成。

用我们的开头示例把链路连起来,就是:

text 复制代码
EntryPlugin
  -> EntryDependency("./src/index.ts")
  -> FactorizeTask
  -> AddTask(index.ts)
  -> BuildTask(index.ts): Loader + Parser
  -> BuildResultTask: 收集 ./math 与 ./lazy
  -> ProcessDependenciesTask
  -> FactorizeTask(math.ts / lazy.ts)
  -> ...直到没有新的 Dependency

最后的收尾:finish make 与清理不可达模块

模块图初步构建完毕后,finish_make_pass() 会调用插件的 finish_make hook。这个阶段允许插件通过 add_include() 补充 include 依赖;源码还专门用 in_finish_make: AtomicBool 限制该 API 只能在此时调用。

最后,finish_module_graph_pass() 会调用 finish_build_module_graph(),内部以 UpdateParam::BuildEntryAndClean 再运行一次更新:

rust 复制代码
// crates/rspack_core/src/compilation/build_module_graph/mod.rs(简化)
pub async fn finish_build_module_graph(...) -> Result<...> {
  update_module_graph(
    compilation,
    artifact,
    exports_info_artifact,
    vec![UpdateParam::BuildEntryAndClean(entry_dependency_ids)],
  ).await
}

和前面的 BuildEntry 相比,BuildEntryAndClean 多了清理动作:只保留从当前入口仍然可达的模块,移除因入口变更或依赖更新而失去连接的旧模块。至此,Make 阶段才真正关闭,后续 Pass 可以把模块图当作稳定输入使用。

这一篇应该带走什么

到这里,我们不需要记住每个文件名,但应该建立起下面这条主线:

text 复制代码
配置 entry
  -> EntryPlugin 在 make hook 中添加 EntryDependency
  -> Cutout 根据入口和变更文件确定重建范围
  -> Task Loop 交替执行:
       后台 Factorize / Build
       主队列 Add / ProcessDependencies / 写 ModuleGraph
  -> finish_make 允许插件补充依赖
  -> BuildEntryAndClean 清理不可达模块
  -> 得到稳定的 ModuleGraph

从 Rust 学习角度看,这里至少有四个值得反复体会的点:

  1. trait object 管理异构任务和异构模块Box<dyn Task<_>>Box<dyn Module> 让运行时可扩展,具体行为仍由 trait 约束;
  2. 所有权换并发 :后台任务不直接持有 &mut ModuleGraph,只产出结果;主任务串行合并结果,因此不用为整张图套一层粗粒度锁;
  3. 阶段 ArtifactBuildModuleGraphArtifact 把本阶段的可变状态聚合起来,既适合缓存,也让后续 Pass 的输入输出更明确;
  4. 更新而非重建Cutout + repair 将首次构建和增量重建统一在同一套算法中。

写在最后

现在我们已经知道 Rspack 是怎样从入口构建 Module Graph 的了。下一步自然就是:有了模块图之后,Rspack 怎么根据同步依赖、动态 import()、入口配置和 splitChunks 规则,把模块组织成 Chunk Graph?

下一篇我会继续沿着 BuildChunkGraphPass 往下读,重点看 ChunkChunkGroupEntrypoint 这些 Rust 数据结构是如何协作的,以及 import() 为什么会带来一个异步 Chunk。

相关推荐
晴天161 小时前
浏览器中ESM与AMD模块共存的解决方案
前端·node.js
芳心粽伙饭1 小时前
CSS第一章 CSS引入
前端·css
雪芽蓝域zzs2 小时前
第三十二节:部门组织管理(el‑tree 组织树
前端·javascript·vue.js
leoZ2312 小时前
第 7 篇:进阶——校验、联动、列表页
开发语言·前端·javascript·vue.js·人工智能·目标检测·ecmascript
VeryCool2 小时前
别吹了,依赖图像识别的GPT‑6 Astra永远快不起来
前端·javascript·aigc
hunterandroid2 小时前
Android 测试全景:从单元测试到 UI 自动化的完整实践
android·前端
leoZ2312 小时前
第 8 篇:与 AI 协作的工作流 + 完整案例
前端·人工智能·神经网络·自然语言处理·性能优化·c#·php
背对疾风2 小时前
提前还贷,缩短年限和降低月供其实是一样的
前端
2501_928996222 小时前
Agent 开发 API 选型:硅碳相变下 Function Calling 兼容性与多模型路由拆解
前端