Webpackの『Statsデータ』を使いこなす:生成されたJSONレポートから不要なPolyfillを特定してバンドルサイズを削る技術
バンドルサイズ肥大化の犯人は、大抵の場合「自らが書いたコード」ではない。見えないところで他人が依存しているポリフィルや、トランスパイルの過剰な安全性担保――つまり、「現代のブラウザ環境においては完全に不要な互換レイヤー」である。
フロントエンド開発の現場において、Webpackは長年ワークフローの心臓部として機能してきたが、その出力物を「なんとなく動くから」と放置しているチームは、CI/CDパイプラインやエンドユーザーのブラウザメモリに対して慢性的な負荷をかけ続けている。
本稿では、Webpackの内部構造の核心であるStats(統計)データをプログラム的にハックし、不要なPolyfillを暴き出し、CIパイプライン上でこれを完全自動で排除するアーキテクチャを解説する。
—
1. Webpack Statsデータの内部アーキテクチャと低レイヤの真実
多くの開発者は、`webpack –json > stats.json` というコマンドを一度は叩いたことがあるだろう。しかし、この数万行〜数十万行に及ぶJSONが、Webpackの内部コンパイラ(Compiler / Compilation)のどのようなメモリ構造をシリアライズしたものかまで理解している者は少ない。
モジュールグラフ(Module Graph)の全貌
Webpack 5では、内部の依存関係管理が従来の `Dependencies Block` から `ModuleGraph` および `ChunkGraph` アーキテクチャへと完全に刷新された。Statsデータとは、このグラフ構造をツリー状、あるいは隣接リスト形式にダンプしたスナップショットである。
[Entrypoint]
└── Chunk (main)
├── Module A (App Code)
│ └── Imported Module B (npm package)
│ └── Polyfill X (core-js/modules/es.array.flat-map.js) <- 🎯ここを狙い撃つ
└── Module C (Vendor)
Statsデータの深淵を覗くと、各モジュールがどのリソースから参照され(`reasons` フィールド)、なぜバンドルに含まれることになったのか(`issuerPath`)の因果関係がすべて記録されている。不要なPolyfillを削るということは、この依存関係の鎖(Chain)のどこを断ち切るか、あるいはどのトランスパイルターゲットを修正すべきかを、データドリブンに特定する作業に他ならない。
---
2. 現場で使える:Statsデータ生成と解析自動化スクリプト
単にJSONを出力するだけでは意味がない。これをCI/CDパイプラインやローカル環境で即座に解析し、バンドル汚染源を炙り出すための実用的なNode.jsスクリプトを構築する。
Step 1: Webpack設定で詳細なStatsを出力させる
まず、Webpackの設定ファイル(`webpack.config.js`)において、モジュール間の依存関係や理由(Reasons)が完全に記録されるようStatsオプションをチューニングする。
// webpack.config.js
module.exports = {
mode: ‘production’,
entry: ‘./src/index.js’,
output: {
filename: ‘[name].[contenthash].js’,
path: __dirname + ‘/dist’,
},
stats: {
// 依存関係の理由(どのファイルからインポートされたか)を追跡するために必須
reasons: true,
// モジュールのソースコード量や依存先を詳細に出力
modules: true,
// チャンクの詳細情報を出力
chunks: true,
// 資産(アセット)のサイズ情報を出力
assets: true,
},
// 以下、babel-loaderやswc-loaderなどの設定が続く…
};
Step 2: 犯人(不要なPolyfill)を特定する解析CLIスクリプト
生成された `stats.json` を読み込み、`core-js` や `regenerator-runtime` などのPolyfill関連モジュールがどこから持ち込まれているかを逆引きするカスタムNode.jsスクリプト(`analyze-polyfills.js`)を作成する。
// analyze-polyfills.js
const fs = require(‘fs’);
const path = require(‘path’);
// Statsファイルのパスを指定
const statsFilePath = path.resolve(__dirname, ‘stats.json’);
if (!fs.existsSync(statsFilePath)) {
console.error(‘Error: stats.json が存在しません。webpack –json > stats.json を実行してください。’);
process.exit(1);
}
const stats = JSON.parse(fs.readFileSync(statsFilePath, ‘utf8’));
// Polyfillやトランスパイルヘルパーの検知パターン
const POLYFILL_PATTERNS = [
‘core-js’,
‘@babel/runtime’,
‘regenerator-runtime’,
‘whatwg-fetch’
];
console.log(‘=== 🔍 バンドル内 Polyfill / ヘルパー 侵入経路分析レポート ===\n’);
// Webpack 5 の stats 構造(modules または 階層化された children を走査)
const modules = stats.modules || (stats.children && stats.children[0].modules);
if (!modules) {
console.error(‘Error: Statsデータからモジュール情報を取得できませんでした。webpack.config.js の stats 設定を確認してください。’);
process.exit(1);
}
let foundCount = 0;
modules.forEach((module) => {
const moduleName = module.name || module.identifier || ”;
// Polyfillパターンにヒットするかチェック
const matchedPattern = POLYFILL_PATTERNS.find(pattern => moduleName.includes(pattern));
if (matchedPattern) {
foundCount++;
console.log(`[検知] パターン: “${matchedPattern}”`);
console.log(` └ モジュール: ${moduleName}`);
console.log(` └ サイズ: ${module.size} bytes`);
// 誰がこのモジュールを引き込んだのか(reasonsを解析)
if (module.reasons && module.reasons.length > 0) {
console.log(‘ └ 侵入経路 (Reasons):’);
module.reasons.forEach(reason => {
console.log(1, ` – 呼び出し元: ${reason.moduleName || reason.userRequest || ‘不明’}`);
});
}
console.log(”);
}
});
console.log(`=================================================`);
console.log(`分析完了: 疑わしいモジュールが ${foundCount} 件検知されました。\n`);
このスクリプトを実行することで、どの外部ライブラリが古いJS仕様のPolyfillを勝手にバンドルへねじ込んでいるのかが丸裸になる。
—
3. 根本治療:モダンブラウザ向け Target 設定とトランスパイルの最適化
原因を特定したら、次はWebpackのビルドパイプライン、あるいはBabel/SWCのトランスパイル設定を修正し、不要なPolyfillの生成そのものを根絶する。
Babel + Browserslistの最適化
多くのプロジェクトでやってしまいがちな致命的ミスが、`browserslist` の設定を曖昧にしたまま `useBuiltIns: ‘usage’` や `core-js` を導入することである。これにより、IE11や古いSafariをサポート対象に入れた瞬間、モダンブラウザにまで数万バイトの無駄なPolyfillが配信される。
最新のモダンブラウザ(ES2020/ES2022標準対応)に絞った `browserslist` を設定し、ビルド成果物を劇的に軽量化する。
// package.json または .browserslistrc
{
“browserslist”: [
“last 2 Chrome versions”,
“last 2 Firefox versions”,
“last 2 Safari versions”,
“last 2 Edge versions”,
“not IE 11”
]
}
さらに、Babelを使用している場合は、`babel.config.js` の設定を精査する。
// babel.config.js
module.exports = {
presets: [
[
‘@babel/preset-env’,
{
// ターゲットブラウザは browserslistrc に準拠
useBuiltIns: ‘usage’, // 実際に使われたコードのみPolyfillをインポート
corejs: { version: 3, proposals: false }, // 最新のcore-js v3を指定
// デバッグを有効にすると、どのファイルにどのpolyfillが挿入されたかビルド時に標準出力される
debug: true,
},
],
],
};
—
4. Dockerコンテナ環境での完全自動構成とCI/CDパイプライン連携
ローカルでの手動解析で満足してはならない。DevOpsの観点において、「バンドルサイズの肥大化や不要なPolyfillの混入は、CIパイプラインで自動検知し、閾値を超えたらビルドを即座に失敗(Fail)させる」のがプロフェッショナルの実装である。
ここでは、Dockerを用いたクリーンなビルド環境と、GitHub ActionsやGitLab CI等で使える自動検証フローのアーキテクチャを構築する。
Dockerfile (マルチステージビルドによる解析環境)
本番用の軽量イメージとは別に、Stats解析とサイズ検証に特化したCI用ステージ、あるいは専用のコンテナ定義を用意する。
—- Build & Analyze Stage —-
FROM node:20-alpine AS builder
WORKDIR /app
依存関係のキャッシュ効率化のため、先にpackage.json等をコピー
COPY package.json package-lock.json ./
RUN npm ci
ソースコードと設定ファイルをコピー
COPY . .
1. Webpackビルドを実行し、Stats JSONを吐き出す
RUN npx webpack –json > stats.json
2. 解析スクリプトを実行し、不要なポリフィルが一定数以上あるか検証
RUN node analyze-polyfills.js
—- Production Artifacts Stage —-
FROM nginx:alpine AS runner
COPY –from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD [“nginx”, “-g”, “daemon off;”]
CIパイプラインへの組み込み(GitHub Actions の例)
もしDockerを使わずにネイティブランナーで回す場合でも、以下のようにCI上でStatsデータを生成し、サイズ回帰テスト(Regression Test)を行うステップを必ず組み込む。
.github/workflows/bundle-audit.yml
name: Bundle Size & Polyfill Audit
on:
pull_request:
branches: [ main ]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’
- name: Install Dependencies
run: npm ci
- name: Generate Webpack Stats
run: npx webpack –json > stats.json
- name: Run Polyfill & Dependency Analyzer
run: node analyze-polyfills.js
- name: Optional – Size Limit Check (bundlesize or similar)
uses: preactjs/compressed-size-action@v5
with:
repo-token: “${{ secrets.GITHUB_TOKEN }}”
build-script: “build”
—
5. エキスパートの知見:メモリ消費とビルドパフォーマンスの最適化ハック
WebpackのStatsデータ生成は、大規模なコードベース(数千・数万モジュール)において、極めて重いCPU処理とメモリ消費を引き起こす。CI環境で `JavaScript heap out of memory` エラーに直面したエンジニアも多いはずだ。
この問題を回避し、高速かつ安全にStatsデータを活用するための低レイヤハックを伝授する。
1. Node.jsのヒープメモリ上限の拡張
大規模バンドルのStats出力を伴うビルドを行う場合、Node.jsのデフォルトメモリ制限(通常は1.4GB〜4GB程度)では耐えられない。環境変数で明示的にヒープサイズを拡張する。
export NODE_OPTIONS=”–max-old-space-size=8192″
npx webpack –json > stats.json
2. 不要なデータのエクスポート抑制(軽量化)
WebpackのStatsオブジェクトは、デフォルトでは「すべてのモジュールのソースコード文字列」や「詳細なAST情報」までJSONに含めようとするため、ファイルサイズが数十MB〜数百MBに膨れ上がる。
`webpack.config.js` の `stats` オプションをチューニングし、解析に不要なプロパティを削ることで、JSON生成のオーバーヘッドとメモリ消費を劇的に削減できる。
// webpack.config.js の最適化された stats 設定
stats: {
all: false, // 一度すべての出力をオフにする
modules: true, // モジュールリストは有効化
reasons: true, // 依存関係の理由は維持
errors: true, // エラーは出力
warnings: true, // 警告は出力
assets: true, // アセットサイズは出力
// 以下、メモリを圧迫する巨大なプロパティを明示的に無効化
source: false,
modulesSpace: 0,
nestedModules: false,
}
—
結び:技術至上主義のアーキテクチャへ向けて
「なんとなくライブラリを入れ、なんとなくトランスパイルし、肥大化したバンドルをそのままCDNに載せる」――そんな時代は終わった。
WebpackのStatsデータを手中に収め、JSONの奥底に潜むモジュールグラフの繋がりを解読し、不要なPolyfillをコードの根底から断ち切る。この一連のプロセスを自動化・標準化することこそが、エンドユーザーの体感速度を極限まで高め、インフラコストを最適化する一流のDevOpsエンジニアの仕事である。
あなたのプロジェクトの `stats.json` には、今、何の「無駄」が隠れているか?
今すぐコマンドを叩き、その目で真実を確かめてほしい。