5月27日 16:49

Rspack Source Map 是如何工作的?

Source Map 是前端构建工具的关键调试能力:它记录编译产物到源码的映射关系,让浏览器 DevTools 中显示的是你写的原始代码,而非经过打包、转译后的产物。Rspack 在这一块做了什么,和 webpack 有什么差异,各 devtool 选项该怎么选?下面逐步拆解。

Source Map 的基本原理

Source Map 本质是一个 JSON 文件(或内联 Base64 字符串),核心字段包括:

  • version:Source Map 规范版本,当前为 3
  • sources:原始源文件路径列表
  • mappings:VLQ 编码的映射信息,记录生成代码的行列号到源码行列号的对应关系
  • names:映射中引用的标识符

浏览器加载编译后的 JS 文件时,如果发现末尾有 //# sourceMappingURL=xxx.map 注释,就会请求该 Source Map 文件,借助 mappings 字段将报错堆栈还原到源码位置。

Rspack Source Map 的工作流程

Rspack 生成 Source Map 经历以下阶段:

  1. 模块解析阶段:每个模块被解析时,Rspack 会记录原始源码的位置信息。如果 Loader(如 babel-loader、swc-loader)本身输出了 Source Map,Rspack 会将其作为输入继续传递。

  2. 模块链式转换阶段:当多个 Loader 依次处理同一个模块时,Source Map 会沿 Loader 链逐层合并。这就是 module 后缀的作用——它会将 Loader 产生的 Source Map 纳入最终结果,而不是只保留 Rspack 自身生成的映射。

  3. Chunk 拼接阶段:多个模块被拼接到同一个 Chunk 中,Rspack 需要调整每个模块的 Source Map 偏移量,确保拼接后的行号仍然能正确映射回源码。

  4. 输出阶段:根据 devtool 配置,决定 Source Map 是内联到 JS 文件中(DataUrl)、生成独立 .map 文件,还是通过 eval() 包裹。

Rspack 与 webpack 的差异

Rspack 的 Source Map 实现兼容 webpack 的 devtool 配置体系,但底层使用 Rust 实现,生成速度显著更快。需要注意的几个差异点:

  • Rspack 的 devtool 选项与 webpack 语义一致,但部分组合(如 @ 前缀的 preload 模式)尚未支持
  • Rspack 在 cheap 模式下的构建速度优势更明显,因为 Rust 的行级映射处理比 Node.js 快得多
  • SourceMapDevToolPluginEvalSourceMapDevToolPlugin 已支持,可作为 devtool 的细粒度替代

devtool 选项详解与选型

Rspack 支持的 devtool 值由几个关键词组合而成,理解这些关键词就能掌握全部选项:

关键词含义
evaleval() 包裹模块,通过 //# sourceURL 关联源文件,不生成真正的 Source Map
source-map生成独立的 .map 文件,完整映射
cheap只生成行级映射,不包含列信息,构建更快
module包含 Loader 的 Source Map,映射到原始源码(如 JSX/TS)而非转译后的代码
hidden生成 .map 文件但不添加 //# sourceMappingURL 注释
nosourcesSource Map 中不包含源码内容,只有位置映射
inlineSource Map 以 DataUrl 内联到 JS 文件中,不生成独立文件

开发环境推荐

javascript
module.exports = { mode: 'development', devtool: 'eval-cheap-module-source-map' }

为什么选 eval-cheap-module-source-map

  • eval 使模块独立执行,增量构建时只重新生成变化的模块,rebuild 速度极快
  • cheap 跳过列映射,大多数调试场景只需要定位到行
  • module 确保 Loader 转换前的源码被映射,你在 DevTools 中看到的是 TSX/JSX 而非编译后的 JS

如果需要精确到列的断点调试,可升级为 eval-source-map,代价是首次构建稍慢。

生产环境推荐

根据目标场景选择:

javascript
// 场景一:需要完整调试信息(内部工具/不公开部署) module.exports = { mode: 'production', devtool: 'source-map' } // 场景二:配合 Sentry 等错误追踪平台,不向用户暴露 Source Map module.exports = { mode: 'production', devtool: 'hidden-source-map' } // 场景三:只暴露错误位置,不暴露源码内容 module.exports = { mode: 'production', devtool: 'nosources-source-map' }

安全提醒:生产环境的 .map 文件绝不能部署到公开 CDN。使用 hidden-source-map 时,Source Map 文件生成但不被浏览器自动加载,你可以将其上传到错误追踪服务后删除。

SourceMapDevToolPlugin 细粒度控制

devtool 的组合选项不满足需求时,可以用 SourceMapDevToolPlugin 精确控制:

javascript
const { SourceMapDevToolPlugin } = require('@rspack/core'); module.exports = { plugins: [ new SourceMapDevToolPlugin({ // 输出文件名模板 filename: '[file].map', // 排除 node_modules,减小 Source Map 体积 exclude: [/node_modules/], // 控制是否追加 sourceMappingURL 注释 append: '\n//# sourceMappingURL=[url]', // 是否生成列映射,false 等同于 cheap columns: true }) ] }

关键参数说明:

  • filename:支持 [file][chunkHash] 等模板变量,设为 false 则内联到 JS
  • exclude / include:按模块路径过滤,避免为第三方库生成 Source Map
  • columns:设为 false 可跳过列映射,效果等同于 cheap,显著提升构建速度
  • append:设为 false 则不添加 sourceMappingURL 注释,等同于 hidden

Source Map 与错误追踪集成

Sentry 集成

javascript
const SentryWebpackPlugin = require('@sentry/webpack-plugin'); module.exports = { mode: 'production', devtool: 'hidden-source-map', plugins: [ new SentryWebpackPlugin({ authToken: process.env.SENTRY_AUTH_TOKEN, org: 'your-org', project: 'your-project', include: './dist', // 上传后可在构建流水线中删除 .map 文件 rewrite: true }) ] }

本地还原错误堆栈

当没有错误追踪平台时,可以用 source-map 库手动还原:

javascript
const { SourceMapConsumer } = require('source-map'); const fs = require('fs'); async function resolveError(line, column) { const rawSourceMap = JSON.parse(fs.readFileSync('./dist/main.js.map', 'utf8')); const consumer = await new SourceMapConsumer(rawSourceMap); const pos = consumer.originalPositionFor({ line, column }); console.log(`源码位置:${pos.source}:${pos.line}:${pos.column}`); consumer.destroy(); }

性能影响与优化策略

Source Map 对构建的影响主要体现在三个维度:

构建时间

不同 devtool 的首次构建时间大致排序(从快到慢):

shell
eval < eval-cheap-source-map < eval-cheap-module-source-map < eval-source-map < source-map

cheap 跳过列映射可节省 30%-50% 的 Source Map 生成时间;eval 通过缓存模块结果让增量构建几乎瞬间完成。

产物体积

独立 .map 文件通常比原文件大 2-5 倍,因为包含了完整的源码和映射信息。内联模式下 Source Map 以 Base64 编码直接嵌入 JS,体积膨胀更为明显。

内存占用

Rspack 在构建过程中需要将 Source Map 数据保存在内存中。对于超大型项目,source-map 模式可能导致内存压力,此时 cheap 模式是更务实的选择。

优化实践

  • 排除 node_modules:第三方库的 Source Map 对调试无意义,使用 SourceMapDevToolPlugin.exclude 过滤
  • 开发环境优先选 eval 系列:增量构建速度差异可达 10 倍以上
  • 生产环境单独生成:在 CI 中生成 .map 文件上传到错误追踪服务后删除,不进入部署产物
  • 监控构建内存:如果 source-map 模式下内存溢出,降级为 cheap-source-map

常见问题排查

Source Map 不生效

检查以下原因:

  1. devtool 是否配置为 false(none)
  2. Loader 是否正确传递 Source Map(如 babel-loader 需要设置 sourceMaps: true
  3. 浏览器 DevTools 中是否开启了 Source Map 功能(Settings → Enable JavaScript source maps)
  4. hidden-source-map 模式下浏览器不会自动加载,这是预期行为

行号对不上

通常是因为缺少 module 关键词。cheap-source-map 映射到 Loader 转译后的代码,而 cheap-module-source-map 会追溯到 Loader 之前的原始源码。如果你的代码经过 babel/swc 转译,必须使用 module 变体才能得到正确的行号。

生产环境 Source Map 泄露

如果 .map 文件可以被公开访问,检查:

  • 构建产物是否包含了 .map 文件
  • sourceMappingURL 是否被意外包含(应使用 hidden-source-map
  • 服务器是否正确配置了 .map 文件的访问控制
标签:Rspack