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 天生就有的模块,而是 EntryPlugin 在 make 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.ts、resolve.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.ts 和 other.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_identifier 是 None。

第六步: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 才从处理后的源码中找出 import、require、import() 等依赖。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. 阶段 Artifact :BuildModuleGraphArtifact 把本阶段的可变状态聚合起来,既适合缓存,也让后续 Pass 的输入输出更明确;
  4. 更新而非重建 :Cutout + repair 将首次构建和增量重建统一在同一套算法中。

写在最后

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

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

相关推荐
笃行35015 小时前
集中式与分布式,KingbaseES 的四种形态共用一套内核
前端
陈随易16 小时前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员
ZhengEnCi17 小时前
12 种高级 UI 交互效果及对应 AI 提示词:从按压回弹到底部抽屉的物理感教学
前端·css·交互设计
IT_陈寒20 小时前
Redis大key删除为什么会导致服务卡顿?我的线上事故复盘
前端·人工智能·后端
用户27050777252420 小时前
MCP 最小 Server 实测:3 个 JSON-RPC 报错与修法
前端
用户4993350250720 小时前
一人VIP账号多人共用:Web 代理里的会话共享怎么实现
前端
用户老李20 小时前
无线电自学教程 05:第一台收音机——从矿石机到 SDR
前端
IPIPD21 小时前
Node.js请求遇到503怎么重试?先解析Retry-After,再做有上限退避
前端
葡萄城技术团队21 小时前
dsh-spreadjs-editor 的集成链路:把 SpreadJS Designer 嵌进 Agent 工作台
前端
FlyWIHTSKY1 天前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js