はじめに
最近のフロントエンド開発では、Viteやesbuildといった高速なビルドツールが注目を集めています。「もうwebpackは古い」「今から学ぶ意味はあるのか」と感じている方も多いのではないでしょうか。
しかし実際の現場を見渡すと、Next.jsのビルドパイプラインや大規模な既存プロジェクトなど、webpackは今も数多くの場所で現役です。特にModule Federationのような、他のツールにはない機能を求めてあえてwebpackを選ぶケースも少なくありません。
この記事では、webpackがどういう仕組みでモジュールをバンドルしているのかを基礎から整理し、実践的な設定パターンまで解説します。
webpackとは
webpackは、JavaScriptを中心としたモジュールバンドラーです。ES ModulesやCommonJSで書かれたファイル同士の依存関係を解析し、ブラウザで実行できる1つ(または複数)のファイルにまとめてくれます。JavaScriptだけでなく、Loaderという仕組みを使うことでCSSや画像、フォントなども「モジュール」として扱えるのが大きな特徴です。
主な特徴
- 柔軟なLoader/Pluginシステム - Babelでのトランスパイル、CSSの取り込み、画像の最適化など、あらゆる処理をパイプラインに組み込める
- Code Splitting - アプリを複数のチャンクに分割し、必要な時に必要な分だけ読み込ませられる
- Module Federation - 別々にビルドされた複数のアプリケーション間でモジュールを共有できる、マイクロフロントエンドの中核技術
- 豊富なエコシステムと実績 - 10年以上の運用実績があり、大規模プロジェクトでの知見やプラグインが蓄積されている
インストール
npm、yarn、pnpmのいずれでもインストールできます。CLIはwebpack-cliとして別パッケージになっているため、あわせてインストールします。
npm install --save-dev webpack webpack-cli
yarn add --dev webpack webpack-cli
pnpm add --save-dev webpack webpack-cli
開発時にホットリロードを使いたい場合は、webpack-dev-serverも一緒に入れておくと便利です。
npm install --save-dev webpack-dev-server
基本的な使い方
webpackはプロジェクトルートに置いたwebpack.config.jsをもとに、エントリーポイントから依存関係をたどってバンドルを生成します。まずは最小構成を見てみましょう。
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production', // 'development' も指定可能
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true, // ビルドごとにdistディレクトリを掃除する
},
};
src/index.jsが他のモジュールをimportしていれば、それらも自動的に解析されdist/bundle.jsにまとめられます。
// src/greet.js
export function greet(name) {
return `こんにちは、${name}さん!`;
}
// src/index.js
import { greet } from './greet.js';
document.body.textContent = greet('webpack');
ビルドはCLI経由で実行します。
npx webpack --config webpack.config.js
package.jsonにスクリプトとして登録しておくのが一般的です。
{
"scripts": {
"build": "webpack --mode production",
"dev": "webpack serve --mode development"
}
}
実践的なユースケース
webpackは設定次第で用途が大きく広がります。ここでは代表的な3つのパターンを紹介します。
CSS・画像をモジュールとして扱う(Loader)
webpackはデフォルトではJavaScriptしか理解できません。CSSや画像をimportできるようにするには、対応するLoaderをmodule.rulesに追加します。webpack 5以降は画像やフォントの取り込みに専用のLoaderが不要になり、組み込みのAsset Modulesで対応できます。
// webpack.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|svg)$/,
type: 'asset/resource', // 画像はファイルとして出力される
},
],
},
};
// src/index.js
import './style.css';
import logo from './logo.svg';
const img = document.createElement('img');
img.src = logo;
document.body.append(img);
CSSはstyle-loaderがJavaScript経由で<style>タグとして注入し、画像はファイルとして出力されつつ、コード内では実際のURLに解決されます。
Code Splittingで初期表示を軽くする
すべてのコードを1つのファイルにまとめると、アプリが大きくなるほど初期表示が遅くなります。動的import()を使うと、そのモジュールは自動的に別チャンクとして分割され、必要になったタイミングで読み込まれます。
// src/index.js
document.getElementById('load-btn').addEventListener('click', async () => {
// クリックされるまでheavy-feature.jsはダウンロードされない
const { runHeavyFeature } = await import('./heavy-feature.js');
runHeavyFeature();
});
// webpack.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js', // チャンクごとにハッシュ付きファイル名
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all', // node_modules由来のコードもvendorチャンクに分離
},
},
};
splitChunksを有効にすると、複数のエントリーで共通利用されているライブラリ(例えばReactなど)を自動的にvendorチャンクへ切り出してくれるため、キャッシュ効率も向上します。
Module Federationでアプリ間をつなぐ(マイクロフロントエンド)
複数のチームがそれぞれ別々にビルド・デプロイしているアプリケーション同士で、コンポーネントやロジックを共有したい場合に使えるのがModule Federationです。ビルド時ではなく実行時にモジュールを読み込むため、片方のアプリだけを再デプロイしても他方に影響しません。
// app-remote/webpack.config.js(機能を提供する側)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
mode: 'production',
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.js', // 他アプリに公開するモジュール
},
shared: ['react', 'react-dom'], // 依存の重複読み込みを避ける
}),
],
};
// app-host/webpack.config.js(機能を利用する側)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
mode: 'production',
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@https://example.com/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};
// app-host/src/index.js
import('remoteApp/Button').then(({ default: Button }) => {
// 別アプリとして公開されたButtonをそのまま利用できる
document.body.append(Button());
});
大規模な組織で複数チームが並行開発している場合、Module FederationはViteやesbuildにはない強力な選択肢になります。
まとめ
webpackは、Loaderによる柔軟なアセット処理、Code Splittingによるパフォーマンス最適化、そしてModule Federationによるマイクロフロントエンド対応という、他のバンドラーにはない領域をカバーしています。開発体験のシンプルさではViteに軍配が上がる場面も多いですが、既存の大規模プロジェクトの保守や、複数アプリを疎結合に連携させたい場面では、依然として有力な選択肢です。
「もう古い」と切り捨てる前に、まずは自分のプロジェクトでwebpack.config.jsを書いてみて、その仕組みを体感してみてください。バンドラーが内部で何をしているかを理解しておくと、Viteなど他のツールを使う際の解像度も上がるはずです。