【webpack速習】vol.8: Code Splitting
下記ページの要点をまとめていきます。
- Entry Points
- Prevent Duplication(重複を防ぐ)
- Dynamic Imports(動的インポート)
- Prefetching/Preloading modules
- Bundle Analysis(バンドル分析)
- コード分割(Code Splitting)は、webpackの最も魅力的な機能の1つ
- この機能を使用すると、コードをさまざまなバンドルに分割して、それらをオンデマンド(注文対応)または並行して読み込むことができる
- より小さなバンドルを作成し、リソース負荷の優先順位付けを制御するために使用できる。これを正しく使用すると、ロード時間に大きなプラス影響を与える可能性がある
- 利用可能なコード分割には3つの一般的なアプローチがある
Entry Points
: エントリ設定を使用してコードを手動で分割するPrevent Duplication
:SplitChunksPlugin
を使用してチャンクを重複排除および分割するDynamic Imports
: モジュール内のインライン関数呼び出しを介してコードを分割する
Entry Points
これは、コードを分割する最も簡単で直感的な方法である。
しかしながら、それはより手動で、いくつかの落とし穴がある。 メインのバンドルから他のモジュールを分割する方法を見てみる。
▼ ディレクトリ構成
project
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
▼ src/another-module.js(新規作成)
import _ from 'lodash'; console.log( _.join(['Another', 'module', 'loaded!'], ' ') );
▼ src/index.js
import _ from 'lodash'; function component() { var element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component());
▼ webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
+ another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
これでビルドすると下記のような出力結果となる。
... Asset Size Chunks Chunk Names another.bundle.js 550 KiB another [emitted] another index.bundle.js 550 KiB index [emitted] index Entrypoint index = index.bundle.js Entrypoint another = another.bundle.js ...
エントリーポイントのファイル間に重複したモジュールがある場合(今回の場合は lodash)、それぞれのバンドルに含まれてしまっているのが問題。
この問題を解決するために SplitChunksPlugin
を使う。
Prevent Duplication(重複を防ぐ)
SplitChunksPlugin
を使うと、共通の依存関係(重複モジュール)を、既存のエントリーファイルまたは別の新しいファイルに抽出できる。
上記を例に、lodashの重複を排除してみる。
※CommonsChunksPlugin
はwebpack v4 legatoから削除された。
▼ webpack.config.js
module.exports = { // 略 + optimization: { + splitChunks: { + chunks: 'all' + } + } }
これでビルドすると出力結果は下記になる。
... Asset Size Chunks Chunk Names another.bundle.js 5.95 KiB another [emitted] another index.bundle.js 5.89 KiB index [emitted] index vendors~another~index.bundle.js 547 KiB vendors~another~index [emitted] vendors~another~index Entrypoint index = vendors~another~index.bundle.js index.bundle.js Entrypoint another = vendors~another~index.bundle.js another.bundle.js ...
index.bundle.js と another.bundle.js から lodash が削除され、新しくできた vendors~another~index.bunlde.js
に lodash が抽出されているのが分かる。
※より詳しい SplitChunksPlugin の使い方は下記を参照
SplitChunksPlugin | webpack
code Splittingするためにその他にも便利なプラグインやローダーがコミュニティから提供されている。
mini-css-extract-plugin
: アプリケーションから CSSを分割するのに役立つbundle-loader
: コードを分割し、結果のバンドルを遅延ロードするために使用されるpromise-loader
: bundle-loader と似ているが promise を利用する
Dynamic Imports(動的インポート)
動的な code splitting に関しては、webpackでは2つの類似した方法がサポートされている。
- 【推奨】dynamic imports のためのECMAScript proposal に準拠した
import()
構文を使用 - 【レガシー】
require.ensure
を使用
ここでは最初の dynamic import
の使い方を見てみる。
※ import()
呼び出しは内部的に promise を使用しているため、古いブラウザで import()
を使用する場合は、es6-promise
や promise-polyfill
などの polyfill を使用する。
▼ webpack.config.js
module.exports = { entry: { index: './src/index.js', - another: './src/another-module.js' }, output: { filename: '[name].bunlde.js', + chunkFilename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, - optimization: { - splitChunks: { - chunks: 'all' - } - } // 略 }
エントリーでないチャンクファイルの名前を chunkFilename
プロパティに設定する。
chunkFilename
の詳細については、下記参照。
Output | webpack
では lodash
を静的に import する代わりに dynamic import
を使用してチャンクを分離する。
▼ src/index.js
- import _ from 'lodash'; - - function component() { + function getComponent() { - var element = document.createElement('div'); - - // Lodash, now imported by this script - element.innerHTML = _.join(['Hello', 'webpack'], ' '); + return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => { + var element = document.createElement('div'); + element.innerHTML = _.join(['Hello', 'webpack'], ' '); + return element; + }).catch(error => 'An error occurred while loading the component'); } - document.body.appendChild(component()); + getComponent().then(component => { + document.body.appendChild(component); + })
default
が必要なのは、webpack 4以降、CommonJSモジュールを import する際に、import が module.exports の値に解決されなくなるため。代わりに、CommonJSモジュール用の人工名前空間オブジェクトが作成される。詳細は下記。
https://medium.com/webpack/webpack-4-import-and-commonjs-d619d626b655
import(/* webpackChunkName: "lodash" */ 'lodash')
の書き方に関して、コメント上で /* webpackChunkName: "lodash" */
と記述することで バンドルの名前は [id] .bundle.js
ではなく lodash.bundle.js
となる。
※webpackChunkName
と他の利用可能なオプションについての詳細は下記。
Module Methods | webpack
これでビルド実行すると下記のようになる。
... Asset Size Chunks Chunk Names index.bundle.js 7.88 KiB index [emitted] index vendors~lodash.bundle.js 547 KiB vendors~lodash [emitted] vendors~lodash Entrypoint index = index.bundle.js ...
htmlからは、index.bundle.js
を読み込むだけで良い。
async function を使う
async function を使うことでもっとすっきりと書くことができる。
ただし、使用には Babel や Syntax Dynamic Import Babel Plugin
のようなプリプロセッサを使用する必要がある。
▼ src/index.js
async function getComponent () { const element = document.getElementBy('div'); const { default: _ } = await import(/* webpackChunkName: 'lodash' */, 'lodash'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } // 略
Prefetching/Preloading modules
webpack 4.6.0以降では、プリフェッチとプリロードのサポートが追加されている。
ここの節に関しては、現在すぐには使用しなそうなのでこの場ではスキップ。
Bundle Analysis(バンドル分析)
コードの分割を開始したら、出力を分析してモジュールがどこで終了したかを確認すると良い。
公式の分析ツールは、最初に分析を始めるのに良いツール。
他にもコミュニティでサポートされているものがある。
- webpack-chart
- webpack-visualizer
- webpack-bundle-analyzer
- webpack bundle optimize helper: