WebpackからRspackへの移行完全ガイド - 実践的な移行手順とトラブルシューティング


はじめに

WebpackからRspackへの移行は、ビルド時間を劇的に短縮できる最も効果的な方法の一つです。本記事では、実際のプロジェクトでの移行経験を基に、段階的な移行手順とよくある問題の解決方法を解説します。

なぜ移行するのか

ビルド時間の短縮:
  webpack: 45秒 → Rspack: 4.8秒 (9.4倍高速)

HMRの高速化:
  webpack: 800ms → Rspack: 120ms (6.7倍高速)

メモリ使用量の削減:
  webpack: 2.8GB → Rspack: 1.2GB (57%削減)

移行の難易度

プロジェクトタイプ移行難易度推定作業時間
基本的なReactアプリ1-2時間
TypeScript + CSS Modules2-4時間
複雑なwebpack設定1-2日
カスタムloaderあり最高3-5日

移行前の準備

互換性チェックリスト

移行前に以下を確認してください。

# 1. Node.jsバージョン確認
node -v  # v16.0.0以上

# 2. webpackバージョン確認
npm list webpack  # 4.x または 5.x

# 3. 使用中のloaderとpluginをリスト化
grep -r "loader" webpack.config.js
grep -r "Plugin" webpack.config.js

互換性マトリックス

// ✅ 完全互換
'style-loader'
'css-loader'
'postcss-loader'
'sass-loader'
'less-loader'
'file-loader' (asset/resource推奨)
'url-loader' (asset/inline推奨)

// ⚠️ 部分互換(要テスト)
'babel-loader' (builtin:swc-loaderに置換推奨)
'ts-loader' (builtin:swc-loaderに置換推奨)
'thread-loader' (Rspackはデフォルトで並列処理)

// ❌ 非互換
'cache-loader' (Rspackの組み込みキャッシュ使用)
'happypack' (Rspackの並列処理で不要)

プロジェクトのバックアップ

# Gitで現在の状態をコミット
git add .
git commit -m "Before Rspack migration"

# バックアップブランチ作成
git checkout -b backup-before-rspack
git checkout main

# または直接バックアップ
tar -czf project-backup.tar.gz ./

段階的移行手順

Phase 1: Rspackのインストール

# Rspack関連パッケージをインストール
npm install -D @rspack/core @rspack/cli

# Reactプロジェクトの場合
npm install -D @rspack/plugin-react-refresh

# TypeScriptプロジェクトの場合(追加不要、組み込み)
# builtin:swc-loaderが標準装備

Phase 2: 設定ファイルの作成

# webpack.config.jsをコピー
cp webpack.config.js rspack.config.js

基本的な変換例:

// webpack.config.js(変更前)
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    new MiniCssExtractPlugin(),
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
};
// rspack.config.js(変更後)
const rspack = require('@rspack/core');
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  plugins: [
    // HtmlWebpackPlugin → HtmlRspackPlugin
    new rspack.HtmlRspackPlugin({
      template: './public/index.html',
    }),
    // MiniCssExtractPlugin → CssExtractRspackPlugin
    new rspack.CssExtractRspackPlugin(),
    // webpack.DefinePlugin → rspack.DefinePlugin
    new rspack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
  ],
};

Phase 3: Loaderの変換

babel-loader → builtin:swc-loader

// webpack.config.js(変更前)
{
  test: /\.(js|jsx)$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: [
        '@babel/preset-env',
        '@babel/preset-react',
      ],
    },
  },
}

// rspack.config.js(変更後)
{
  test: /\.(js|jsx)$/,
  exclude: /node_modules/,
  use: {
    loader: 'builtin:swc-loader',
    options: {
      jsc: {
        parser: {
          syntax: 'ecmascript',
          jsx: true,
        },
        transform: {
          react: {
            runtime: 'automatic',
          },
        },
      },
    },
  },
}

ts-loader → builtin:swc-loader

// webpack.config.js(変更前)
{
  test: /\.tsx?$/,
  exclude: /node_modules/,
  use: 'ts-loader',
}

// rspack.config.js(変更後)
{
  test: /\.tsx?$/,
  exclude: /node_modules/,
  use: {
    loader: 'builtin:swc-loader',
    options: {
      jsc: {
        parser: {
          syntax: 'typescript',
          tsx: true,
        },
      },
    },
  },
}

CSS Loaders

// CSS(変更なし)
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader'],
  type: 'javascript/auto', // ⚠️ Rspackでは必須
}

// CSS Modules
{
  test: /\.module\.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: {
          localIdentName: '[name]__[local]--[hash:base64:5]',
        },
      },
    },
  ],
  type: 'javascript/auto',
}

// SCSS
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader'],
  type: 'javascript/auto',
}

アセットローダー

// webpack(変更前)
{
  test: /\.(png|jpg|gif)$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        name: '[name].[hash].[ext]',
      },
    },
  ],
}

// Rspack(変更後)- webpack 5スタイル
{
  test: /\.(png|jpg|gif)$/,
  type: 'asset/resource',
  generator: {
    filename: '[name].[hash][ext]',
  },
}

// SVGの処理
{
  test: /\.svg$/,
  type: 'asset/resource',
}

Phase 4: Pluginの変換

主要なPlugin変換表

// HTML生成
webpack.HtmlWebpackPlugin → rspack.HtmlRspackPlugin

// CSS抽出
MiniCssExtractPlugin → rspack.CssExtractRspackPlugin

// 環境変数
webpack.DefinePlugin → rspack.DefinePlugin

// プログレス
webpack.ProgressPlugin → rspack.ProgressPlugin

// コピー
CopyWebpackPlugin → rspack.CopyRspackPlugin

実装例

const rspack = require('@rspack/core');
const ReactRefreshPlugin = require('@rspack/plugin-react-refresh');

const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  plugins: [
    new rspack.HtmlRspackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.ico',
    }),
    new rspack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.API_URL': JSON.stringify(process.env.API_URL),
    }),
    new rspack.CopyRspackPlugin({
      patterns: [
        { from: 'public/static', to: 'static' },
      ],
    }),
    isDev && new ReactRefreshPlugin(),
  ].filter(Boolean),
};

Phase 5: DevServerの設定

// webpack.config.js(変更前)
devServer: {
  port: 3000,
  hot: true,
  historyApiFallback: true,
  proxy: {
    '/api': 'http://localhost:8080',
  },
}

// rspack.config.js(変更後)- ほぼ同じ
devServer: {
  port: 3000,
  hot: true,
  historyApiFallback: true,
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
    },
  },
}

Phase 6: package.jsonの更新

{
  "scripts": {
    "dev": "rspack serve --mode development",
    "build": "rspack build --mode production",
    "preview": "rspack serve --mode production",
    "analyze": "ANALYZE=true rspack build --mode production"
  },
  "devDependencies": {
    "@rspack/core": "^1.0.0",
    "@rspack/cli": "^1.0.0",
    "@rspack/plugin-react-refresh": "^1.0.0"
  }
}

実プロジェクト移行例

例1: Create React Appからの移行

# 1. CRA設定をejectせずに移行
npx @rspack/cli init

# 2. 必要な設定を追加
# rspack.config.js
module.exports = {
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'builtin:swc-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
        type: 'javascript/auto',
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new rspack.HtmlRspackPlugin({
      template: './public/index.html',
    }),
  ],
  devServer: {
    port: 3000,
    hot: true,
  },
};

例2: Next.js風のディレクトリ構造

// rspack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
      '@hooks': path.resolve(__dirname, 'src/hooks'),
      '@styles': path.resolve(__dirname, 'src/styles'),
    },
  },
};

例3: モノレポでの移行

// packages/web/rspack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      '@shared': path.resolve(__dirname, '../../packages/shared/src'),
    },
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        shared: {
          test: /[\\/]packages[\\/]shared[\\/]/,
          name: 'shared',
          chunks: 'all',
        },
      },
    },
  },
};

トラブルシューティング

エラー1: Module not found

エラー: Module not found: Error: Can't resolve 'xxx'

原因:
- resolve.extensionsが不足
- resolve.aliasの設定ミス

解決策:
module.exports = {
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
};

エラー2: Loaderが動作しない

エラー: You may need an appropriate loader to handle this file type

原因:
- type: 'javascript/auto'が不足(CSS系)
- loaderの順序が間違っている

解決策:
{
  test: /\.css$/,
  use: ['style-loader', 'css-loader'], // 右から左に実行
  type: 'javascript/auto', // CSS系は必須
}

エラー3: HMRが動作しない

原因:
- React Refresh Pluginが不足
- devServer.hotがfalse

解決策:
const ReactRefreshPlugin = require('@rspack/plugin-react-refresh');

module.exports = {
  plugins: [
    new ReactRefreshPlugin(),
  ],
  devServer: {
    hot: true,
  },
};

エラー4: 環境変数が読めない

// ❌ 動かない
console.log(process.env.REACT_APP_API_URL);

// ✅ DefinePluginで定義
new rspack.DefinePlugin({
  'process.env.REACT_APP_API_URL': JSON.stringify(process.env.REACT_APP_API_URL),
});

エラー5: CSS Modulesのクラス名が衝突

// ❌ localIdentNameが短すぎる
{
  loader: 'css-loader',
  options: {
    modules: {
      localIdentName: '[hash:base64:5]',
    },
  },
}

// ✅ ファイル名とローカル名を含める
{
  loader: 'css-loader',
  options: {
    modules: {
      localIdentName: '[name]__[local]--[hash:base64:5]',
    },
  },
}

パフォーマンス最適化

キャッシュの有効化

module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
};

Code Splitting

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
        },
        common: {
          minChunks: 2,
          name: 'common',
          priority: 5,
        },
      },
    },
  },
};

バンドルサイズ分析

npm install -D webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    process.env.ANALYZE && new BundleAnalyzerPlugin(),
  ].filter(Boolean),
};
ANALYZE=true npm run build

移行チェックリスト

□ Node.js v16以上をインストール済み
□ Gitでバックアップ作成済み
□ @rspack/core, @rspack/cliをインストール
□ rspack.config.jsを作成
□ loader設定を変換
  □ babel-loader → builtin:swc-loader
  □ ts-loader → builtin:swc-loader
  □ CSS loaderに type: 'javascript/auto' 追加
□ plugin設定を変換
  □ HtmlWebpackPlugin → HtmlRspackPlugin
  □ MiniCssExtractPlugin → CssExtractRspackPlugin
  □ DefinePlugin → rspack.DefinePlugin
□ devServer設定を確認
□ package.jsonのscriptsを更新
□ 開発サーバーで動作確認
  □ HMRが動作する
  □ 環境変数が読める
  □ CSSが適用される
□ 本番ビルドで動作確認
  □ エラーなくビルド完了
  □ バンドルサイズを確認
  □ 実際にデプロイして確認

まとめ

移行のメリット

  1. ビルド時間: 10倍以上高速化
  2. HMR: 6倍以上高速化
  3. メモリ: 50%以上削減
  4. 開発体験: ストレスフリー

ベストプラクティス

  • 段階的に移行(1プロジェクトずつ)
  • 開発環境で十分テスト
  • バンドルサイズを比較
  • キャッシュを活用

次のステップ

Rspackで開発速度を劇的に向上させましょう。