[キャッチアップ] Rspack
Rust 製の webpack 互換のあるモジュールバンドラ。
まだ実用性の判断もできないけど、とりあえず先行してドキュメントをつまみ食いしていく。
記事化しました。本スクラップには記事に含まれていないメモ書きもあるので残しておきます。
Introduction
- Rspack の発音記号的にはアーレスペックに近い
- Rust 製の高速モジュールバンドラで、 webpack エコシステムとの互換性に力を入れている
なぜ Rspack を作ったのか
元々は社内のビルドシステムの問題解決のために始まった。
多くのバンドラツールに求められる要件を調査した結果が以下だった。
- 開発環境の起動が高速であること
- CI/CD でのビルドが高速であること
- 柔軟に設定可能であること
- 本番環境用の最適化能力があること
上記を満たせるものがなかった (例えば webpack は低速ではあるが柔軟なカスタマイズがウリである)
Rspack の現状
-
2022/04 から開発を始め、2023/03 に OSS 公開したばかりでアーリーステージ
-
Webpack 互換の機能もまだまだ不足している
-
とはいえパレートの原則に基づけば、既に多くのニーズを満たせて入る
-
社内プロジェクトでは既に webpack から移行しているものもあり、5〜10倍の高速化も出来ている
-
Rspack は webpack のスキーマ、ローダーといったアーキテクチャと互換性を持っている
-
babel-loader
less
loader -
将来的には
vue-loader
といった主要なローダーをすべてサポートする -
現状はオンメモリのシンプルなキャッシュ機構を持っている
-
将来的には永続キャッシュを導入して、クラウドキャッシュを再利用するなども実現可能にしたい
Rspack のこれから
Rspack は webpack との完全な互換性を目指して、コミュニティからのフィードバックをベースに開発優先順位を付けている。
− コミュニティパートナーとの協働
- プラグインの互換性の強化
- 既に基本的な loader インタフェースと、いくつかのプラグインの実装は完了済み
- 100% 互換を目指すわけであはないが、基本的な開発に用いられているものについてはフィードバックを元に対応する
- 継続的なパフォーマンスの向上
- Rspack はパフォーマンスを最大のウリとして開発している
- 並列化アルゴリズムやキャッシュ戦略を強化してさらなるパフォーマンスを求める
- テストカバレッジの向上
- 既に主要なwebpack 互換に関するテストを対応済み
- リリース間の互換性を保つためのテストスイートも広げていく
Webpack との違い
webpack はおそらく最も普及したバンドラで、活発なエコシステム、柔軟なカスタマイズ性、そして機能性がウリである。
webpack と rspack の違いは以下の通り。
- Rust により圧倒的なパフォーマンス
- そもそも JS よりも速い言語を採用
- 並列実行に特化したアーキテクチャ
- JS では言語の制約上不可能だった、マルチコアを活用したアーキテクチャを Rust で実現している
- 主要機能をビルトインで揃えている
- webpack はそこについてもサードパーティに頼らざるを得なく、それがボトルネックになりやすい
- 最適化された HMR
- インクリメンタルコンパイル戦略によって、プロジェクトの規模に関わらないスピードを出せる
Vite との違い
Vite は開発体験としては良いが、プロダクションビルドには Rollup を使用しており、JavaScript の制約による限界がある。
esbuild との違い
esbuild は golang を用いてパフォーマンスの向上を行ったが、webpack のような機能は持ち合わせていない。
Turbopack との違い
Turbopack は Rspack と同じで Rust を採用してパフォーマンスの向上を図っているが、webpack との互換性を持たない再設計を行っているため、マイグレーションが困難である。
FAQ
Rspack と Webpack の関係は?
- webpack チームとはパートナーシップを結んでいる
- Rspack の目的は webpack のパフォーマンスを Rust を用いて解決することと
- webpack チームと協力して、さらなる webpack の可能性も探求する
Webpack ないし JavaScript エコシステムとの互換性を保つとパフォーマンス劣化しない?
- 劣化はするが、ビジネスの到達点を考えると妥協できる劣化に過ぎない
- ベンチマークを追いすぎてマイグレーションコストを無視しない
babel-loader を使わずに babel-loader 互換をどうやって保つ?
- Rspack では SWC を使ってコードのトランスパイルをしている
Webpack に対して 100% 互換する?
- しないし 100% は目指していない
- 多くの場面で使われている主要な機能を優先して対応している
Vue をサポートする?
- 現在作業中
- 4月から6月 での対応を目指してる
Webpack + SWC-loader を使うのと比べるメリットは?
- babel-loader に関わるパフォーマンスの問題は解決できるが、webpack 自体がボトルネックとして残る
- webpack がシングルスレッドで動いている問題を Rspack ではマルチコアで対応できる
カスタムプラグインやカスタムローダは Rust で実装する?
- webpack と同様に、JavaScript を用いて実装する
- Rust を使って実装できる方法も模索している
React Server Component をサポートする?
- 現在のところは予定はない
- コミュニティの動向次第
プロダクションで利用できる?
- できる
- 社内のいくつかのプロジェクトでは既に利用済み
- ランタイムターゲットは概ね webpack と揃えてはいるが、まだ完全ではない
Glossary
Rspack と webpack でよく使われる用語集
asset
- アプリケーション内で利用される、画像や動画、フォントなどのリソースのこと
- base64に変換されコード内にインラインで展開されたり、出力ディレクトリに配置されたりする
asset module
- 画像、動画、フォントなどのアセットをモジュールとして扱ったもの
bundle
- 従来はソースコードを一つのファイルに束ねたものをそう呼んでいた
- 現代ではバンドルは小さなファイル(チャンク) に分解され、オンデマンドで読み込めるようになっているため一つとは限らない
bundle splitting
- ソースコードを分割統合し、複数のバンドルにするテクニック
- 並列リクエストやブラウザキャッシュの効率化に役立つ
chunk
- 複数のモジュールを一つのグループに束ねたファイル
chunk graph
- チャンク同士の関連を表現したデータ構造
- グラフは有向グラフっで、チャンク間の依存関係を示す
code splitting
- コードを複数のチャンクに分割するテクニック
- アプリケーションを動かすのに必要な最低限のコードのみ最初に読み込み、以降は必要に応じてチャンクを取得する
first class module type
- RSpack において、追加のローダーやプラグインなしで最初から扱えるモジュールの種類
- JS, CSS, JSON などがファーストクラスモジュール
loader
- モジュールを変換することに特化したプラグインのようなもの
- TypeScript を JavaScript に変換するのもローダー
- CSSモジュールを JavaScript モジュールにするのもローダー
module
- import / export を通じてコードの再利用をできるようにした仕組み
module type
- そのモジュールをバンドラがどのように扱うかで分類された種別
- 例えばモジュールが CSS であれば、バンドラは CSS パーサを使用する
module resolution
- import のようなモジュール指定しによって示されるファイルパスを計算するプロセス
module graph
- モジュール間の依存関係を表現したデータ構造
- グラフは有向グラフっで、チャンク間の依存関係を示す
NAPI_RS
- Rustでコンパイル済みのNode.jsアドオンを構築するためのフレームワーク
plugin
- Rspack の機能を拡張するプログラム
- 各プロセスにフックして任意のプロセスを追加できる
tree shaking
- バンドルから未使用コードを除外するテクニック
- Rspack では自動でデッドコードを検出して削除する
Language support
TypeScript
- TS はビルトインでサポートされている
- トランスパイルには SWC を使用している
- よって webpack から移行する場合は、
babel-loader
ts-loader
の設定は除外できる - 型チェックは行わないので、必要に応じて tsc などを組み合わせる必要がある
- よって webpack から移行する場合は、
- 並列実行を最大限に活かすために、
isolatedModules
を設定する必要がある- よって enum や namespace を使用することはできない
JSX and TSX
- JSX/TSX はビルトインでサポートされている
- Node に対するポリフィルは自動で挿入しないので、手動で
@rspack/plugin-node-polyfill
を設定する
Static resource handling
- 画像、動画、フォントなどの静的リソースのバンドルもビルトインでサポートしている
- 以下は
.png
を静的リソースとしてバンドルするための設定
module.exports = {
module: {
rules: [
{
test: /\.png$/,
type: 'asset',
},
],
},
};
CSS
- CSS はビルトインでサポートされている
- webpack から移行する場合、
css-loader
style-loader
は不要になる
- webpack から移行する場合、
- .css ファイルは CSS モジュールタイプとして扱われ、
.module.css
は CSS Modules モジュールタイプとして扱われる - ややこし!
- 前者は css-loader に当たるものが適用され、後者は CSS Modules による JavaScript モジュールとなる
- postcss-loader 実装済み
- less-loader 実装済み
- sass-loader 実装済み
- Tailwind CSS は postcss と組み合わせて利用可能
JSON
- JSONはビルトインでサポートされている
Asset modules
画像や動画、フォントといった静的ファイルをモジュールとして扱うアセットモジュールは、Rspack ではびつロインでサポートされており、追加のローダーなどを必要とせずにそのまま利用することができる。
Supported Asset Module types
アセットモジュールはいくつかのモジュールタイプに分類できる
-
asset/inline
: アセットを Base64 エンコードした DataURI 形式に変換してエクスポートする -
asset/resource
: アセットを単体のファイルとして扱い、ファイルのURLとしてエクスポートする -
asset
: サイズに応じてasset/inline
asset/resource
のいずれかになる- デフォルトでは8KBまでは
asset/inline
になる
- デフォルトでは8KBまでは
-
asset/source
: アセットファイルの内容をそのまま文字列としてエクスポートする
Loader
ローダーはファイルの変換に特化したプラグインのようなもので、Rspack では多くのローダーがビルトインで提供される。
- 例えば less-loader などもすぐに使える
- 複数のローダーを組み合わせるといった使い方も、webpack と同じように設定を書ける
- カスタムローダーを JavaScript で簡単に実装できる
以下はバナーをコンテントの先頭に付与するローダーの実装
const BANNER = `/**
* MIT Licensed
* Copyright (c) 2022-present ByteDance, Inc. and its affiliates.
* https://github.com/web-infra-dev/rspack/blob/main/LICENSE
*/`;
module.exports = function (content) {
return `${BANNER}\n${content}`;
};
ローダーモジュールを取り込んで、すべてのJSファイルに適用できる
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: require.resolve('./banner-loader.js'),
},
],
},
};
Plugins
プラグインはローダーと違い、ビルドプロセス全体に対する拡張を行える。
プラグインは JavaScript で記述し、Rust のプロセス内で実行される。実装する場合は apply
メソッドを持つクラスを作成し、apply
メソッドではコンパイラインスタンスに対して任意の操作ができる。
const PLUGIN_NAME = 'MyPlugin';
class MyPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
console.log('The Rspack build process is starting!');
});
}
}
module.exports = MyPlugin;
commonjs 形式で公開されたクラスを、設定ファイルから読み込めばOK
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [new BundleAnalyzerPlugin()],
};
Module resolution
モジュール解決は、 import 文などに現れるモジュール識別子から、実際のファイルパスを解決するプロセスのこと。
Rspack では Node における解決アルゴリズムを拡張した nodejs_resolver を使用している。
モジュール解決は概ね以下の3パターンサポートされている
絶対パス
import '/home/me/file';
既に参照可能なファイルパスになっているのでそのままでOK
相対パス
import './src/answer';
そのファイルが属しているディレクトリからみた相対パスとして解決される
モジュールパス
import 'lodash';
./
/
などが使用されず、パッケージ名のみ指定したケース。 Node.js のアルゴリズムに基づいて、 node_modules
ディレクトリなどから解決される
DevServer
webpack-dev-server のような開発用サーバをビルトインで有しており、HMRのような機能と、プロキシサーバーとしての機能を持つ。
Optimization: Production
ソースマップ
- ソースマップ機能を有しており、デバッグのためにもプロダクションでの有効化を推奨している
- プロジェクトが大きく、ソースマップ生成のパフォーマンスが気になる場合は、
source-map
オプションで調整することが可能
ミニフィケーション
- ビルトインで JS や CSS をミニファイする機能を有している
- もちろん無効化や設定変更も可能
Optimization: Code splitting
コード分割は、コードをいくつかのチャンクに分割することで、初期読み込みされるリソースを削減し、パフォーマンスを向上させる機能。
分割には3つの方法がある
- Entry Points
- Prevent Duplication
- Dynamic imports
Entry point
entry
オプションにて、手動でバンドル生成のエントリーファイルを複数指定する手法。
/**
* @type {import('@rspack/core').Configuration}
*/
const config = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
},
};
module.exports = config;
上記では少なくとも index
と another
2つのバンドルを生成している。
この手法では直感的に分割できるが、以下の問題が発生しやすい。
-
index
another
それぞれで共通のモジュールへの依存があっても、それぞれで依存解決を行い、同じコードが両バンドルに含まれてしまう - 手動設定なので柔軟性に欠け、コアロジックを動的に分割する手法が使えなくなる
SplitChunksPlugin
SplitChunksPlugin は、エントリー間で共通するモジュールを抜き出して新たにチャンクを生成するプラグイン。
Dynamic import
import()
シンタックスを利用した、ESM による動的インポート機能を用いてインポートすることで、依存モジュールをチャンクとして切り出すことができる。
import('./shared')
console.log('index.js')
この場合、 shared
は index
のバンドルに含まれずに、非同期で読み込めるようになる。
Optimization: Tree shaking
- ツリーシェイキングは、コード内の到達不能コード(デッドコード) を検出してバンドルから除外してくれる機能
- ESM による import/export での依存グラフを用いて、モジュール内の不使用関数などを除外できる
- ツリーシェイキングは
mode
がproduction
の場合に自動で有効化される - モジュールが副作用を持っている可能性がある場合、パッケージの
sideEffects
オプションに応じて、副作用がツリーシェイキングに影響するかを判断する
Optimization: Bundle analysis
rspack build --analyze
で、 webpack-bundle-analyzer
を使用したバンドル分析結果を生成できる
Framework support: React
- JSX/TSX がビルトインでサポートされているため、React 開発は最初から行える
- Emotion/Fast Refresh / SVG コンポーネントもビルドインサポート
Framework support: SolidJS
- babel-loader の solidjs 用のプリセットを使用するだけで、SolidJS の利用が可能
Framework support: Vue
Vue の完全なサポートを提供予定(≒今は不完全)
vue-loader
vue-loader の完全なサポートを目指しているが、現状 Rspack が提供する API の制限の都合まだ出来ていない。
代わりに vue-loader 相当の機能を自分で実装することは可能
Vue 3 JSX
@vue/babel-plugin-jsx
を直接使用することはできるので、JSX シンタックスは扱える
Compatibility: Loader compat
互換性のあるローダー一覧
Compatibility: Plugin conpat
互換性のあるプラグイン一覧
Migration from webpack
Rspack は Webpack との高い互換性を目指してい入るが、実装の違いによりいくつかのマイグレーションプロセスが必要になる
設定ファイルのマイグレーション
設定ファイルにも互換性があったりなかったりするので、フィールドごとの互換性を確認し、書き換える必要がある。
ローダーの置き換え
-
babel-loader
は多くの場合不要に- コードダウングレードはビルトインでサポート
- TypeScript/JSX もビルトインでサポート
- 他の理由で
babel-loader
を使用している場合は継続的に利用可能
-
css-loader
style-loader
mini-css-extract-plugin
も不要に -
file-loader
url-loader
も不要- (webpack 5 では既に不要になってる)
プラグインの置き換え
-
html-webpack-plugin
はビルトイン機能に移動