はじめに
先日リリースされたwebpack 4でCommonsChunkPluginは廃止されました(CommonsChunkPluginの解説はこちら)。
その代わりとした追加されたoptimization.splitChunks
の基本的な使い方、使い所に関しての記事です。
optimization.splitChunks
を利用すればサイトパフォーマンスの改善(詳細は後述)ができるため、利用する機会が多い設定(機能)です。
解説に利用しているコードの最終形態はGitHub上にあります。
hira777/webpack-split-chunks-example
webpackを理解していることを前提とした記事ですので、基礎知識を習得したい方はwebpack 4入門をご覧ください。
optimization.splitChunks
とは
「複数のエントリーポイント間で利用している共通モジュールをバンドルしたファイル」を出力するための設定のこと。
設定はwebpack.config.js
に記述する。
通常の出力とoptimization.splitChunks
を有効にした出力の違い(イメージ図)
以下は通常の出力
以下はoptimization.splitChunks
を有効にした出力
なぜoptimization.splitChunks
を有効にするのか
上記のイメージ図のように共通ファイルを出力するため、以下のようなメリットがあるから。
- 複数のエントリーポイントが1つの共通ファイルを利用するため、全体のファイルサイズが小さくなる
- 共通ファイルなのでキャッシュを活用できる(1度読み込めば別ページに遷移しても読み込みが不要、共通ではないファイルだけ読み込めば良い)
optimization.splitChunks
を有効にしてみる
webpack.config.js
にoptimization.splitChunks
の設定を記述して共通のモジュールをバンドルしたファイルを出力してみる。
ディレクトリ構成
.
├── package.json
├── public
│ ├── index.html
│ ├── index2.html
│ ├── index3.html
│ └── js
│ ├── app.bundle.js
│ ├── app2.bundle.js
│ ├── app3.bundle.js
│ └── vendor.bundle.js
├── src
│ └── js
│ ├── app.js
│ ├── app2.js
│ └── app3.js
└── webpack.config.js
各ファイルの詳細
package.json
{
"name": "webpack-split-chunks-example",
"version": "1.0.0",
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0"
},
"dependencies": {
"jquery": "^3.4.0",
"velocity-animate": "^1.5.2"
}
}
上記のpackage.json
が存在する階層で以下のコマンドを実行すれば、必要なパッケージ(モジュール)をインストールできる。
npm install
or
yarn install
webpack.config.js
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
},
optimization: {
splitChunks: {
name: 'vendor',
chunks: 'initial',
}
}
};
以下がoptimization.splitChunks
に関する記述である。
optimization: {
splitChunks: {
name: 'vendor',
chunks: 'initial',
}
}
name
共通モジュールをバンドルしたファイル(チャンク)の名前。今回は'vendor'
を指定したため、出力されるファイル名はvendor.bundle.js
になる。
true
にすると自動的に名前がつけられる。
chunks
共通モジュールとしてバンドルする対象の設定。initial
、async
、all
が存在する。
基本的な利用であればinitial
を指定しておけば良い。
バンドルしたファイルを複数に分割して出力し、非同期で読み込む場合などは状況に応じてasync
やall
を指定する。
app.js
、app2.js
エントリーポイントであるapp.js
、app2.js
は以下の処理を記述している。
- セレクタを指定し、文字列を出力。
- 指定したセレクタをフェードアウトする。
import $ from 'jquery';
import velocity from 'velocity-animate';
const text = 'app';
const $body = $('body');
$body.html(text);
velocity($body, 'fadeOut', {
duration: 1000
});
import $ from 'jquery';
import velocity from 'velocity-animate';
const text = 'app2';
const $body = $('body');
$body.html(text);
velocity($body, 'fadeOut', {
duration: 1000
});
どちらも外部モジュールであるjquery
とvelocity-animate
を利用している。
そのため、jquery
とvelocity-animate
は複数のエントリーポイント間で利用されている共通のモジュールである。
app3.js
エントリーポイントであるapp3.js
は以下の処理を記述している。
const body = document.getElementsByTagName('body')[0];
body.innerText = 'Body!!';
body
要素に文字列を出力しているだけのため、モジュールは何も利用していない。
webpackコマンドでバンドルしたファイルを出力
上記構成のwebpack.config.js
が存在する階層でwebpack --mode production
(もしくはwebpack --mode development
)コマンドを実行すれば、それぞれバンドルしたファイルが出力される。
何が共通モジュールなのかはwebpackが自動で判別し、共通モジュールをバンドルしたファイル(今回はvendor.bundle.js
)を出力する。
今回の設定だと以下のファイルがpublic/js/
に出力される。
app.bundle.js
app2.bundle.js
app3.bundle.js
vendor.bundle.js
webpack --mode production
# 以下のような出力がされる
Hash: 71b17588b6f95673e281
Version: webpack 4.30.0
Time: 436ms
Built at: 2019-04-20 23:19:44
Asset Size Chunks Chunk Names
app.bundle.js 1.57 KiB 1 [emitted] app
app2.bundle.js 1.57 KiB 2 [emitted] app2
app3.bundle.js 991 bytes 3 [emitted] app3
vendor.bundle.js 130 KiB 0 [emitted] vendor
Entrypoint app = vendor.bundle.js app.bundle.js
Entrypoint app2 = vendor.bundle.js app2.bundle.js
Entrypoint app3 = app3.bundle.js
[2] ./src/js/app.js 179 bytes {1} [built]
[3] ./src/js/app2.js 180 bytes {2} [built]
[4] ./src/js/app3.js 81 bytes {3} [built]
+ 2 hidden modules
どのエントリーポイントの共通モジュールがバンドルされたのかは、コマンド実行時の出力で確認できる。
上記の出力を確認してみると、vendor.bundle.js
がapp.bundle.js
とapp2.bundle.js
の共通モジュールをバンドルしている(依存関係を持っている)ことがわかる。
Entrypoint app = vendor.bundle.js app.bundle.js
Entrypoint app2 = vendor.bundle.js app2.bundle.js
Entrypoint app3 = app3.bundle.js
出力されたファイルを確認してみる
それぞれのエントリーポイントから出力されたファイルを読み込んでいるHTMLは以下の通り。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="js/vendor.bundle.js"></script>
<script src="js/app.bundle.js"></script>
</body>
</html>
index2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="js/vendor.bundle.js"></script>
<script src="js/app2.bundle.js"></script>
</body>
</html>
index3.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="js/app3.bundle.js"></script>
</body>
</html>
全て正常に動作していることがわかる。
今回の場合、共通モジュールを利用しているapp.bundle.js
とapp2.bundle.js
はvendor.bundle.js
も読み込むと正常に動作する。
app3.bundle.js
は共通モジュールを利用していないため、vendor.bundle.js
自体を読み込む必要がなく、正常に動作する。
optimization.splitChunks
を有効にしないとどのようなファイルが出力されるのか
通常の出力と何が違うのかを理解するために、optimization.splitChunks
を有効にしないとどのようなファイルが出力するのか見てみる。
以下はoptimization.splitChunks
の記述を削除したwebpack.config.js
。
const path = require('path');
module.exports = {
entry: {
app: './src/app.js',
app2: './src/app2.js',
app3: './src/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
}
};
上記の設定でwebpack --mode production
(もしくはwebpack --mode development
)コマンドを実行すると以下のようなファイルが出力される。
webpack
# 以下のような出力がされる
Hash: 2a40542b654de233f5af
Version: webpack 4.30.0
Time: 2198ms
Built at: 2019-04-20 23:22:43
Asset Size Chunks Chunk Names
app.bundle.js 131 KiB 0 [emitted] app
app2.bundle.js 131 KiB 1 [emitted] app2
app3.bundle.js 991 bytes 2 [emitted] app3
Entrypoint app = app.bundle.js
Entrypoint app2 = app2.bundle.js
Entrypoint app3 = app3.bundle.js
[2] ./src/js/app.js 179 bytes {0} [built]
[3] ./src/js/app2.js 180 bytes {1} [built]
[4] ./src/js/app3.js 81 bytes {2} [built]
+ 2 hidden modules
このファイルでもブラウザ上で正常に動作するが、モジュールを利用しているapp.js
とapp2.js
のそれぞれにjquery
とvelocity-animate
がバンドルされて出力されてしまっている。
そのため、先ほどoptimization.splitChunks
を有効にして出力したファイルと比べ、以下の問題点がある。
- エントリーポイントを更新する度に
jquery
とvelocity-animate
もバンドルしたファイルが更新されるため、キャッシュを活用できない -
app.bundle.js
とapp2.bundle.js
のファイルサイズが大きくなってしまう - それぞれのエントリーポイントに同じモジュールを読み込んでいるため、無駄にファイルサイズが大きくなっている
ということでoptimization.splitChunks
を有効にすれば、キャッシュを活用するだけではなく、全体のファイルサイズも少なくできる。
optimization.splitChunks
の様々な設定や使い方
上記は最低限の利用方法のため、利用シーンに応じた設定や使い方をいくつか紹介する。
optimization.splitChunks
でバンドルする共通モジュールを指定する
特に指定をしなければoptimization.splitChunks
は自動で共通モジュールをバンドルして出力をする。
今回はjquery
とvelocity-animate
が共通で利用されているため、それらがバンドルされて出力される。
自動で共通モジュールをバンドルする機能は便利だが、その機能がこちらの意図に削ぐわない時もある。
前述した構成にユーザーが作成したmoduleA.js
を追加するシーンを想定してみる。
ディレクトリ構成は以下のようになる。
.
├── README.md
├── package.json
├── public
│ ├── index.html
│ ├── index2.html
│ ├── index3.html
│ └── js
│ ├── app.bundle.js
│ ├── app2.bundle.js
│ ├── app3.bundle.js
│ └── vendor.bundle.js
├── src
│ └── js
│ ├── app.js
│ ├── app2.js
│ ├── app3.js
│ └── modules
│ └── moduleA.js
└── webpack.config.js
moduleA.js
の内容は以下の通り。
export default function () {
console.log('moduleA!!!');
console.log('moduleA!!!');
console.log('moduleA!!!');
console.log('moduleA!!!');
console.log('moduleA!!!');
};
上記モジュールを以下のようにapp.js
とapp2.js
で読みこんで利用する。
import $ from 'jquery';
import velocity from 'velocity-animate';
import moduleA from './modules/moduleA';
const text = 'app';
const $body = $('body');
$body.html(text);
velocity($body, 'fadeOut', {
duration: 1000
});
moduleA();
import $ from 'jquery';
import velocity from 'velocity-animate';
import moduleA from './modules/moduleA';
const text = 'app2';
const $body = $('body');
$body.html(text);
velocity($body, 'fadeOut', {
duration: 1000
});
moduleA();
この状態でwebpack --mode production
(もしくはwebpack --mode development
)コマンドを実行するとjquery
、velocity-animate
、moduleA.js
がバンドルされたvendor.bundle.js
が出力される。
自動で共通のモジュールがバンドルされたが、moduleA.js
が頻繁に更新されるモジュールであったり、moduleB.js
のような新しいモジュールが追加される度にvendor.bundle.js
が更新されてしまうため、キャッシュをあまり活用できない。
jquery
、velocity-animate
のような頻繁に更新しない外部モジュール(node_modules
配下のモジュール)のみをバンドルしたい場合、以下のようにwebpack.config.js
に記述を変更する。
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
},
optimization: {
splitChunks: {
// cacheGroups内にバンドルの設定を複数記述できる
cacheGroups: {
// 今回はvendorだが、任意の名前で問題ない
vendor: {
// node_modules配下のモジュールをバンドル対象とする
test: /node_modules/,
name: 'vendor',
chunks: 'initial',
enforce: true
}
}
}
}
};
この状態でwebpack --mode production
(もしくはwebpack --mode development
)コマンドを実行すると以下のようにjquery
、velocity-animate
のみがバンドルされたvendor.bundle.js
が出力され、moduleA.js
はapp.js
とapp2.js
のそれぞれにバンドルされて出力される。
webpack --mode production
# 以下のような出力がされる
Hash: 85dfe516e28462f92c16
Version: webpack 4.30.0
Time: 1996ms
Built at: 2019-04-20 23:26:15
Asset Size Chunks Chunk Names
app.bundle.js 1.76 KiB 1 [emitted] app
app2.bundle.js 1.76 KiB 2 [emitted] app2
app3.bundle.js 991 bytes 3 [emitted] app3
vendor.bundle.js 130 KiB 0 [emitted] vendor
Entrypoint app = vendor.bundle.js app.bundle.js
Entrypoint app2 = vendor.bundle.js app2.bundle.js
Entrypoint app3 = app3.bundle.js
[0] ./src/js/modules/moduleA.js 177 bytes {1} {2} [built]
[3] ./src/js/app.js 231 bytes {1} [built]
[4] ./src/js/app2.js 232 bytes {2} [built]
[5] ./src/js/app3.js 81 bytes {3} [built]
+ 2 hidden modules
共通モジュールをバンドルしたファイルを複数出力する
上記のmoduleA.js
のようなサイズが小さいモジュールであれば、それぞれのエントリーポイントにバンドルしても問題ない。
しかし、サイズが大きいモジュールをそれぞれのエントリーポイントにバンドルしてしまうと、全体のファイルサイズが大きくなる等のデメリットが生じてしまう。
とは言えども、頻繁に更新をしない外部モジュールはそれらだけをバンドルしてキャッシュを活用したい。
そのため、外部モジュールをバンドルしたファイルとは別の「自分たちが作成した共通モジュールをバンドルしたファイル」を出力すれば、全体のファイルサイズが大きくなるデメリットも解消される。
以下のようにwebpack.config.js
に記述を追加すれば、共通モジュールをバンドルしたファイルを複数出力できる。
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
},
optimization: {
splitChunks: {
cacheGroups: {
// 今回はvendorだが、任意の名前で問題ない
vendor: {
// node_modules配下のモジュールをバンドル対象とする
test:/node_modules/,
name: 'vendor',
chunks: 'initial',
enforce: true
},
vendorModules: {
// 今回はsrc/js/modules配下にバンドルしたいモジュールが存在するため指定は以下になる
test: /src\/js\/modules/,
name: 'vendor-modules',
chunks: 'initial',
enforce: true
}
}
}
}
};
この状態でwebpack --mode production
(もしくはwebpack --mode development
)コマンドを実行すると以下のようにjquery
、velocity-animate
がバンドルされたvendor.bundle.js
とmoduleA.js
がバンドルされたvendor-modules.js
が出力される。
webpack --mode production
# 以下のような出力がされる
Hash: 2695ceef4fb3c93f0e96
Version: webpack 4.30.0
Time: 270ms
Built at: 2019-04-20 23:27:55
Asset Size Chunks Chunk Names
app.bundle.js 1.59 KiB 2 [emitted] app
app2.bundle.js 1.59 KiB 3 [emitted] app2
app3.bundle.js 991 bytes 4 [emitted] app3
vendor-modules.bundle.js 238 bytes 1 [emitted] vendor-modules
vendor.bundle.js 130 KiB 0 [emitted] vendor
Entrypoint app = vendor.bundle.js vendor-modules.bundle.js app.bundle.js
Entrypoint app2 = vendor.bundle.js vendor-modules.bundle.js app2.bundle.js
Entrypoint app3 = app3.bundle.js
[2] ./src/js/modules/moduleA.js 177 bytes {1} [built]
[3] ./src/js/app.js 231 bytes {2} [built]
[4] ./src/js/app2.js 232 bytes {3} [built]
[5] ./src/js/app3.js 81 bytes {4} [built]
+ 2 hidden modules
以下のように出力されたvendor-modules.js
も読み込めば動作する。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="js/vendor-modules.bundle.js"></script>
<script src="js/vendor.bundle.js"></script>
<script src="js/app.bundle.js"></script>
</body>
</html>
optimization.splitChunks
のデフォルト設定を理解し、上書きする
webpack実行時、以下のoptimization.splitChunks
に関する設定がデフォルトで有効になっている。
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
つまり、以下の設定でwebpackを実行すれば
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
}
};
以下の設定でwebpackが実行されているのと同じである。
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
},
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
設定項目
各設定項目は以下の通り(まだ解説していないものだけ記載)
※一部項目があまり理解できていないため、間違いがあれば修正を加えていきます。
minSize
共通モジュールとしてバンドルするモジュールの最小サイズ(単位はbyte)。
デフォルトが30000のため、30kb未満のモジュールは複数のエントリーポイントで利用されていても共通モジュールとしてバンドルされない。
minChunks
モジュールがいくつのエントリーポイントで利用されていれば、共通モジュールとしてバンドルするかの設定。
前述したmoduleA.js
は2つのエントリーポイントでしか利用されていないため、minChunks: 3
を指定すると共通モジュールとしてバンドルされない。
maxAsyncRequests
オンデマンドローディング時の最大並列リクエスト数(あまり良くわかっていない)
maxInitialRequests
エントリポイントでの並列リクエストの最大数(あまり良くわかっていない)
cacheGroups
前述したvendor.bundle.js
、vendor-modules.js
のように、指定した共通モジュールを出力したい時に設定する項目。
minSize
やminChunks
なども指定できるため、出力するファイル毎に設定が可能。
priority
cacheGroups
内の設定の優先順位(値が大きいほど優先順位が高い)。
ユーザーが追加したcacheGroups
のpriority
はデフォルトが0のため、デフォルトで存在するdefault
とvendors
より優先度が高い。
そのため、以下の場合どちらもnode_modules
内のモジュールを対象として共通モジュールをバンドルする設定だが、vendor
の設定が優先されてファイルが出力される。
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
vendor: {
test: /node_modules/,
name: "vendor",
minSize: 1,
minChunks: 2,
}
}
}
reuseExistingChunk
モジュールが正確に一致したときに新しいチャンクを作成する代わりに、既存のチャンクを再利用することができる(あまり良くわかっていない)。
設定を上書きする
デフォルト設定ではchunks: 'async'
が指定されているため、この状態でwebpackを実行しても共通モジュールをバンドルしたファイルは出力されないことが多い。
そのため、chunks: 'all'
に上書きしてみる。設定を上書きしたい場合は以下のようにwebpack.config.js
に設定を記述すれば良い。
const path = require('path');
module.exports = {
entry: {
app: './src/js/app.js',
app2: './src/js/app2.js',
app3: './src/js/app3.js',
},
output: {
filename: '[name].bundle.js',
path: path.join(__dirname, 'public/js'),
},
optimization: {
splitChunks: {
// この設定以外はデフォルトのまま
chunks: 'all'
}
}
};
上記の設定でwebpackを実行すると、以下のような出力がされる。
Hash: 756f13664ec4b4102c16
Version: webpack 4.30.0
Time: 306ms
Built at: 2019-04-20 23:36:34
Asset Size Chunks Chunk Names
app.bundle.js 1.76 KiB 1 [emitted] app
app2.bundle.js 1.76 KiB 2 [emitted] app2
app3.bundle.js 991 bytes 3 [emitted] app3
vendors~app~app2.bundle.js 130 KiB 0 [emitted] vendors~app~app2
Entrypoint app = vendors~app~app2.bundle.js app.bundle.js
Entrypoint app2 = vendors~app~app2.bundle.js app2.bundle.js
Entrypoint app3 = app3.bundle.js
[0] ./src/js/modules/moduleA.js 177 bytes {1} {2} [built]
[3] ./src/js/app.js 231 bytes {1} [built]
[4] ./src/js/app2.js 232 bytes {2} [built]
[5] ./src/js/app3.js 81 bytes {3} [built]
+ 2 hidden modules
app.bundle.js
とapp2.bundle.js
の共通モジュールをバンドルしているvendors~app~app2.bundle.js
が出力された。
デフォルト設定であるminSize: 30000
が有効であるため、30kb以下であるmoduleA.js
はapp.js
とapp2.js
のそれぞれにバンドルされて出力される。moduleA.js
も共通モジュールとしてバンドルしたいのであれば、minSize: 1
などを指定すればバンドルされる。
自分でoptimization.splitChunks
の設定をしたのに予期しないファイルが出力された場合、デフォルト設定が有効になっている可能性もあるので注意。
終わり
自分で設定を書いてみて、どんなファイルが出力されるのか確認してみると理解が深まるかと思います。非常に有用な設定ですので、状況に応じて利用していきましょう。
このoptimization.splitChunks
に関しては私が公開した Umdey 講座でも学習できます。
-
webpack 最速入門(
10,800 円-> 2,000 円)
記事と内容が重複している箇所もありますが、optimization.splitChunks
の基本から応用的な使い方までハンズオン形式で学習できます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。
最適化に関連する記事
最適化に関連する webpack を 利用した Tree Shaking に関しての記事を書きましたので、興味があるかはこちらの記事もどうぞ。
webpackのTree Shakingを理解してファイルサイズを削減する
お知らせ
Kindle でも技術書を出版しています。
Kindle(Kindle Unlimited だったら無料):
React Hooks 入門(500 円)
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。