基于 Rust 的高性能 Web 构建工具
导语
Rust这几年以其高性能和可靠性正在快速占领前端基建领域,“任何能够用 Rust 实现的应用系统,最终都必将用 Rust 实现”。今年3月份,字节跳动开源了基于Rust开发的构建工具Rspack,和其他Rust构建工具如turbopack等不同的是,Rspack走了兼容webpack生态的路线,意味着业务能低成本从webpack迁移至Rspack
Rspack简介
下面引用一下Rspack官网的介绍
Rspack(读音为 /’ɑrespæk/,)是一个基于 Rust 的高性能构建引擎, 具备与 Webpack 生态系统的互操作性,可以被 Webpack 项目低成本集成,并提供更好的构建性能。
Rspack 已经完成了对 webpack 主要配置的兼容,并且适配了 webpack 的 loader 架构。目前,你已经可以在 Rspack 中无缝使用你熟悉的各种 loader,如 babel-loader、less-loader、sass-loader 等等。我们的长期目标是完整地支持 loader 特性,未来你可以在 Rspack 中使用那些更加复杂的 loader,如 vue-loader。 目前 Rspack 对缓存支持还比较简单,仅支持了内存级别的缓存,未来我们会建设更强的缓存能力,包括可迁移的持久化缓存,这将带来更大的想象空间,如在 monorepo 里不同的机器上都可以复用 Rspack 的云端缓存,提升大型项目的缓存命中率。
Webpack兼容
Webpack作为最流行的前端构建工具之一,主要原因就是其社区内丰富的Loader、Plugin生态。Rspack兼容了Webpack的主要配置,并适配了对Loader架构的支持,方便渐进式升级,从Webpack低成本迁移到Rspack。值得关注的是,Rspack的目标也不是完全兼容Webpack API,Rspack 目前仍缺失了很多的webpack 的插件Hook 以及一些API,且有些配置会影响构建产物,目前仍在补齐中,不能做到无缝切换。
与其他激进的Rust构建工具如Turbopack不同,Rspack兼容Webpack会带来部分性能损失,相比业界其他Rust构建工具也有性能差距,但性能和迁移成本的平衡也是值得大家关注的,Rspack为业界提供了一个更中庸的方案。目前Rspack团队已经和Webpack团队确立了合作关系,后续可能会探索将Rspack集成到Webpack中。
除了基于 webpack 的构建方式,Rspack还兼容基于 Rollup 的构建方式,能够满足不同应用场景的需求
Rust带来的性能提升
Rspack得益于Rust的高性能编译器支持,Rust 编译生成的 Native Code 通常比 JavaScript 性能更为高效。JavaScript的垃圾回收机制会在程序运行时不断寻找并释放不再使用的对象和变量,Rust的内存管理是通过所有权系统管理内存,并在编译时会根据一系列的规则进行检查,使得 Rust 有惊人的内存利用率。另外,Rust语言对并行化良好的支持,使得Rspack能在模块图生成、代码生成等阶段多线程并行执行,从而充分利用多核CPU的优势,大幅提升编译性能。
Rspack核心逻辑和核心插件都用了rust实现
Webpack性能瓶颈主要来自其js Plugin | Loader,而Rspack基于Rust重写了Webpack的大部分Loader和Plugin,内置了如 Typescript、JSX、CSS、CSS Modules、Sass 等常见构建能力,减小了js Plugin | Loader带来的性能问题和通信开销成本,保证了其构建速度。同时Rspack支持自定义插件和自定义Loader拓展更多能力,有很强的可插拔性和自由度。
Loader兼容
Loader | 兼容性 | 替代 | 备注。 |
---|---|---|---|
babel-loader | 兼容 | 内部使用SWC实现了babel-loader的大部分转换功能 | babel-loader对性能影响较大,请谨慎使用 |
sass-loader | 兼容 | ||
less-loader | 兼容 | ||
postcss-loader | 兼容 | ||
yaml-loader | 兼容 | ||
json-loader | 兼容 | ||
stylus-loader | 兼容 | ||
@mdx-js-loader | 兼容 | ||
@svgr/webpack | 兼容 | ||
raw-loader | 兼容 | type: “asset/source” | |
url-loader | 兼容 | type: “asset/resource” | |
css-loader | 兼容 | type: “css” |
Plugin兼容
Plugin | 兼容性 | 替代 | 备注 |
---|---|---|---|
html-webpack-plugin | 有替代 | builtins.html或者 @rspack/plugin-html | 目前并不支持 webpack 的 childCompiler 相关功能 |
DefinePlugin | 已内置 | builtins.define | |
copy-webpack-plugin | 部分兼容 | builtins.copy | 暂不兼容最新版本,但兼容 copy-webpack-plugin@5 版本 |
mini-css-extract-plugin | 已内置 | type=“css” | |
terser-webpack-plugin | 已内置 | builtins.minifyOptions | |
progressPlugin | 已内置 | builtins.progress | |
webpack-bundle-analyzer | 已内置 | 已内置支持,–analyze 开启 | |
webpack-stats-plugin | 兼容 | ||
tsconfig-paths-webpack-plugin | 已内置 | resolve.tsConfigPath |
需要注意的是,目前自定义Plugin和Loader仍需使用Javascript开发,暂不支持Rust开发。对于一些构建定制化较高的业务,js Plugin | Loader的通信带来的性能损耗也值得关注。Rspack团队后续会探索支持业务使用Rust开发自定义插件和Loader。
缓存与增量编译
Rspack通过文件级别的缓存机制,可以在不同代码版本之间共享缓存,减少重复编译,提高构建速度和开发效率。目前 Rspack 对缓存支持还比较简单,仅支持了内存级别的缓存,未来会建设可迁移的持久化缓存,实现在不同业务复用Rspack的云端缓存。Rspack还在HMR阶段采用了更为高效的增量编译策略,只针对修改过的代码重新编译,HMR速度极快,在大型项目上大大缩短构建时间
Rspack实现
Rspack本身是个非常庞大的系统,包含了很多功能和模块,以下将从Rspack的主要流程出发介绍Rspack实现
温馨提示:Rspack目前仍处在快速迭代阶段,部分功能后续可能有调整
初始化
从repack.config.js、配置文件、Shell命令中读取参数生成配置参数,创建Compiler对象。
(和webpack一样,rspack支持MultiCompiler模块,在不同的compiler中运行不同配置)
const compiler = await cli.createCompiler(rspackOptions, "serve");
const compilers = cli.isMultipleCompiler(compiler) ? compiler.compilers : [compiler];
Compiler对象会在初始化的时候会创建一个binding实例基于napi-rs(napi-rs对大部分常用的 N-API 接口封装成了 Safe Rust 接口),通过N-API(N-API的调用会带来非常大的开销)实现与Rust 编写的native addon通信。
通过node-binding创建Rust的@rspack/core实例,后续的Rspack hooks、build等都通过这个实例处理
#[napi]
impl Rspack {
#[napi(constructor)]
pub fn new(
env: Env,
options: RawOptions,
js_hooks: Option<JsHooks>,
output_filesystem: ThreadsafeNodeFS,
) -> Result<Self> {
init_custom_trace_subscriber(env)?;
// rspack_tracing::enable_tracing_by_env();
Self::prepare_environment(&env);
tracing::info!("raw_options: {:#?}", &options);
let disabled_hooks: DisabledHooks = Default::default();
let mut plugins = Vec::new();
if let Some(js_hooks) = js_hooks {
plugins.push(JsHooksAdapter::from_js_hooks(env, js_hooks, disabled_hooks.clone())?.boxed());
}
let compiler_options = options
.apply(&mut plugins)
.map_err(|e| Error::from_reason(format!("{e}")))?;
tracing::info!("normalized_options: {:#?}", &compiler_options);
let rspack = rspack_core::Compiler::new(
compiler_options,
plugins,
AsyncNodeWritableFileSystem::new(env, output_filesystem)
.map_err(|e| Error::from_reason(format!("Failed to create writable filesystem: {e}",)))?,
);
let id = COMPILER_ID.fetch_add(1, Ordering::SeqCst);
unsafe { COMPILERS.insert_if_vacant(id, rspack) }?;
Ok(Self { id, disabled_hooks })
}
}
#[napi(
catch_unwind,
js_name = "unsafe_build",
ts_args_type = "callback: (err: null | Error) => void"
)]
pub fn build(&self, env: Env, f: JsFunction) -> Result<()> {
let handle_build = |compiler: &mut _| {
// Safety: compiler is stored in a global hashmap, so it's guaranteed to be alive.
let compiler: &'static mut rspack_core::Compiler<AsyncNodeWritableFileSystem> =
unsafe { std::mem::transmute::<&'_ mut _, &'static mut _>(compiler) };
callbackify(env, f, async move {
compiler
.build()
.await
.map_err(|e| Error::new(napi::Status::GenericFailure, format!("{e}")))?;
tracing::info!("build ok");
Ok(())
})
};
unsafe { COMPILERS.borrow_mut(&self.id, handle_build) }
}
对于Rust编写的native addon,不同系统、CPU架构会通过不同的 npm package 分发,这里将所有native package作为 optionalDependencies,并在binding.js中判断当前系统环境require对应的native addon package。
switch (platform) {
case 'linux':
switch (arch) {
case 'x64':
nativeBinding = require('./rspack.linux-x64-musl.node')
break
case 'arm64':
nativeBinding = require('./rspack.linux-arm64-musl.node')
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break;
...
}
module.exports.default = module.exports = nativeBinding
初始化server服务
@rspack/dev-server启动server服务,这里@rspack/dev-server直接继承了webpack-dev-server
import { RspackDevServer } from "@rspack/dev-server";
server = new RspackDevServer(compiler.options.devServer ?? {}, compiler);
await server.start();
启动@rspack/dev-middleware,生成与Rspack的compiler绑定的中间件,然后在前一步启动的server服务中调用这个中间件。通过watch mode监听资源的变更,然后自动打包写入内存发送到 server服务。
启用配置中devServer.devMiddleware中间件
import rdm from "@rspack/dev-middleware";
private setupDevMiddleware() {
// @ts-expect-error
this.middleware = rdm(this.compiler, this.options.devMiddleware);
}
server下通过node_binding api从内存获取文件内容
(现在内存fs会导致outputFileSystem出现问题,Rspack暂时禁用了getRspackMemoryAssets,这意味着每次变更都会将新文件打包到本地而不是通过走内存的方式,这里会对耗时会有影响)
import { getRspackMemoryAssets } from "@rspack/dev-middleware";
if (Array.isArray(this.options.static)) {
this.options.static.forEach(staticOptions => {
staticOptions.publicPath.forEach(publicPath => {
compilers.forEach(compiler => {
if (compiler.options.builtins.noEmitAssets) {
middlewares.push({
name: "rspack-memory-assets",
path: publicPath,
middleware: getRspackMemoryAssets(compiler, this.middleware)
});
}
});
});
});
}
懒编译(Lazy-compilation),只有在用户访问时才编译
compilers.forEach(compiler => {
if (compiler.options.experiments.lazyCompilation) {
middlewares.push({
middleware: (req, res, next) => {
if (req.url.indexOf("/lazy-compilation-web/") > -1) {
const path = req.url.replace("/lazy-compilation-web/", "");
if (fs.existsSync(path)) {
compiler.rebuild(new Set([path]), new Set(), error => {
if (error) {
throw error;
}
res.write("");
res.end();
console.log("lazy compiler success");
});
}
}
}
});
}
});
构建阶段(Make)
根据配置中的 entry 找出所有的入口文件,保存entry对应的entry_dependencies以及module_graph
pub fn setup_entry_dependencies(&mut self) {
self.entries.iter().for_each(|(name, item)| {
let dependencies = item
.import
.iter()
.map(|detail| {
let dependency =
Box::new(EntryDependency::new(detail.to_string())) as BoxModuleDependency;
self.module_graph.add_dependency(dependency)
})
.collect::<Vec<_>>();
self
.entry_dependencies
.insert(name.to_string(), dependencies);
})
}
根据entry依赖生成模块,以及hmr阶段遍历module_graph找到依赖修改的模块,创建一个build_queue调用 loader 将module转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
// move deps bindings module to force_build_module
for dependency_id in &force_build_deps {
if let Some(mid) = self
.module_graph
.module_identifier_by_dependency_id(dependency_id)
{
force_build_module.insert(*mid);
}
}
let mut need_check_isolated_module_ids = HashSet::default();
let mut origin_module_issuers = HashMap::default();
// calc need_check_isolated_module_ids & regen_module_issues
for id in &force_build_module {
if let Some(mgm) = self.module_graph.module_graph_module_by_identifier(id) {
let depended_modules = mgm
.all_depended_modules(&self.module_graph)
.into_iter()
.copied();
need_check_isolated_module_ids.extend(depended_modules);
origin_module_issuers.insert(*id, mgm.get_issuer().clone());
}
}
生成阶段(Seal)
TreeShaking
if option.builtins.tree_shaking {
let (analyze_result, diagnostics) = self
.compilation
.optimize_dependency()
.await?
.split_into_parts();
if !diagnostics.is_empty() {
self.compilation.push_batch_diagnostic(diagnostics);
}
self.compilation.used_symbol_ref = analyze_result.used_symbol_ref;
self.compilation.bailout_module_identifiers = analyze_result.bail_out_module_identifiers;
self.compilation.side_effects_free_modules = analyze_result.side_effects_free_modules;
self.compilation.module_item_map = analyze_result.module_item_map;
// This is only used when testing
#[cfg(debug_assertions)]
{
self.compilation.tree_shaking_result = analyze_result.analyze_results;
}
}
self.compilation.seal(self.plugin_driver.clone()).await?;
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
#[instrument(name = "compilation:seal", skip_all)]
pub async fn seal(&mut self, plugin_driver: SharedPluginDriver) -> Result<()> {
use_code_splitting_cache(self, |compilation| async {
build_chunk_graph(compilation)?;
plugin_driver.write().await.optimize_chunks(compilation)?;
Ok(compilation)
})
.await?;
plugin_driver
.write()
.await
.optimize_chunk_modules(self)
.await?;
plugin_driver.write().await.module_ids(self)?;
plugin_driver.write().await.chunk_ids(self)?;
self.code_generation().await?;
self
.process_runtime_requirements(plugin_driver.clone())
.await?;
self.create_hash(plugin_driver.clone()).await?;
self.create_chunk_assets(plugin_driver.clone()).await;
self.process_assets(plugin_driver).await?;
Ok(())
}
Rspack目前仅支持在make阶段的增量构建,seal阶段还未支持
如何使用
将 webpack 相关包替换为 Rspack 对应的包,并将调用的代码进行替换
- webpack -> @rspack/core
- webpack-dev-server -> @rspack/dev-server
- html-webpack-plugin -> @rspack/plugin-html
将 Rspack 已内置支持的配置替换掉
- style-loader、css-loader、MiniCssExtractPlugin 等 CSS 相关功能在 Rspack 中已内置支持
- TerserWebpackPlugin、CssMinimizerPlugin 等代码压缩功能在 Rspack 中已内置支持,production 模式下默认开启
-
DefinePlugin 在 Rspack 中可通过
builtins.define
进行替代 -
ReactRefreshWebpackPlugin 在 Rspack 中已内置支持,可通过
builtins.react.refresh
开启或关闭
去掉 Rspack 目前还不完整支持的配置
- cache 目前 Rspack 仅支持内存缓存,并会在 development 模式下默认开启
- resolve.plugins 目前 Rspack 还不支持
- CaseSensitivePathsPlugin、ESLintPlugin 等在 Rspack 中目前并不支持
const path = require('path');
module.exports = {
context: __dirname,
mode: 'development',
entry: {
main: ['./src/index.jsx'],
},
builtins: {
html: [{}],
define: {
'process.env.NODE_ENV': '\'development\'',
},
},
module: {
rules: [
{
test: /.less$/,
use: ['less-loader'],
type: 'css',
},
]
},
output: {
path: path.resolve(__dirname, 'dist')
}
};
Webpack对比
前面对Rspack构建流程的介绍我们可以发现,Rspack的架构大部分参考了Webpack,也复用了很多webpack的能力如webpack-dev-server、webpack-dev-middleware等。那么,Rspack对比Webpack能带来多大的性能提升呢?
从Rspack官网的数据可以看到,Rspack构建速度对比Webpack确实很惊人,这主要得益于Rust带来的并行架构和高性能,以及Rspack的增量编译、缓存等优化。Webpack虽然可以通过happypack实现多线程、N-API跑一些Rust SWC-loader等,但由于其本身架构问题性能还是有很大差距。
另外Rspack也和 Webpack 团队确立了合作关系,当 Rspack 达到一定的成熟度时,webpack 团队将尝试以实验特性方式将 Rspack 集成到 webpack 中
总结
目前Rspack仍处在早期阶段,还存在着诸多小问题(如Cache清理、内存io等问题)待处理,在深入源码的过程中也发现了许多优化空间(如seal阶段支持增量编译等),如果考虑在生产环境使用的话可能需要慎重些,建议等待后续稳定版本。
目前字节已开始在内部推广并持续完善中,期待后续能看到更完善的产品。