コード分割はwebpackの最も魅力的な機能の1つです。この機能を使用すると、コードをさまざまなバンドルに分割し、オンデマンドまたは並行してロードできます。これは、より小さなバンドルを実現し、リソースロードの優先順位を制御するために使用でき、正しく使用すれば、ロード時間に大きな影響を与える可能性があります。
利用可能なコード分割には、3つの一般的なアプローチがあります。
entry
設定を使用して、手動でコードを分割します。SplitChunksPlugin
を使用して、チャンクを重複排除および分割します。これは、コードを分割する最も簡単で直感的な方法です。ただし、より手動であり、これから説明するいくつかの落とし穴があります。メインバンドルから別のモジュールを分割する方法を見てみましょう。
プロジェクト
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
これにより、次のビルド結果が得られます。
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
前述のように、このアプローチにはいくつかの落とし穴があります。
これらの2つのポイントの最初のポイントは、./src/index.js
内でもlodash
がインポートされており、両方のバンドルで重複するため、確かに例の問題です。次のセクションでこの重複を削除しましょう。
dependOn
オプションを使用すると、チャンク間でモジュールを共有できます。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
単一のHTMLページで複数のエントリーポイントを使用する場合、optimization.runtimeChunk: 'single'
も必要です。そうしないと、こちらで説明されている問題が発生する可能性があります。
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
そして、これがビルドの結果です。
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
ご覧のとおり、shared.bundle.js
、index.bundle.js
、another.bundle.js
の他に、もう1つ runtime.bundle.js
ファイルが生成されています。
webpackではページごとに複数のエントリーポイントを使用できますが、可能な限り、複数のインポートを持つエントリーポイント (entry: { page: ['./analytics', './app'] }
) を優先して避ける必要があります。これにより、async
スクリプトタグを使用する場合に、より良い最適化と一貫した実行順序が得られます。
SplitChunksPlugin
を使用すると、共通の依存関係を既存のエントリーチャンクまたは完全に新しいチャンクに抽出できます。これを使用して、前の例の lodash
の依存関係を重複排除してみましょう。
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'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
optimization.splitChunks
設定オプションを適用すると、index.bundle.js
と another.bundle.js
から重複した依存関係が削除されているはずです。プラグインは、lodash
を別のチャンクに分離したことを検出し、メインバンドルから不要なものを削除します。npm run build
を実行して、動作するかどうかを確認しましょう。
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms
以下に、コミュニティが提供する、コード分割に役立つ他のプラグインとローダーをいくつか示します。
mini-css-extract-plugin
: CSSをメインアプリケーションから分割するのに役立ちます。webpackでは、動的なコード分割に関して2つの類似した手法がサポートされています。最初で推奨されるアプローチは、動的インポートに関するECMAScriptプロポーザルに準拠するimport()
構文を使用することです。レガシーなwebpack固有のアプローチは、require.ensure
を使用することです。まず、この2つのアプローチの最初のものを試してみましょう。
始める前に、上記の例の余分なentry
と optimization.splitChunks
を設定から削除しましょう。これは次のデモンストレーションには必要ありません。
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'),
},
- optimization: {
- splitChunks: {
- chunks: 'all',
- },
- },
};
また、プロジェクトを更新して、不要になったファイルを削除します。
プロジェクト
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules
さて、lodash
を静的にインポートする代わりに、動的インポートを使用してチャンクを分割します。
src/index.js
-import _ from 'lodash';
-
-function component() {
+function getComponent() {
- const element = document.createElement('div');
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import('lodash')
+ .then(({ default: _ }) => {
+ const element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- return element;
+ 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モジュールをインポートするときに、インポートが module.exports
の値に解決されなくなり、代わりにCommonJSモジュール用の人工的な名前空間オブジェクトが作成されるためです。この理由の詳細については、webpack 4: import() and CommonJs をお読みください。
webpackを実行して、lodash
が別のバンドルに分割されていることを確認しましょう。
...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
./src/index.js 434 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms
import()
はPromiseを返すため、async
関数で使用できます。以下にコードを簡略化する方法を示します。
src/index.js
-function getComponent() {
+async function getComponent() {
+ const element = document.createElement('div');
+ const { default: _ } = await import('lodash');
- return import('lodash')
- .then(({ default: _ }) => {
- const element = document.createElement('div');
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- return element;
- })
- .catch((error) => 'An error occurred while loading the component');
+ return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
Webpack 4.6.0以降では、プリフェッチとプリロードのサポートが追加されました。
インポートを宣言する際にこれらのインラインディレクティブを使用すると、webpackは「リソースヒント」を出力します。これは、ブラウザに以下を伝えます。
この例として、HomePage
コンポーネントがあり、LoginButton
コンポーネントをレンダリングし、クリックされるとオンデマンドでLoginModal
コンポーネントをロードする場合を考えます。
LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
これにより、<link rel="prefetch" href="login-modal-chunk.js">
がページのheadに追加され、ブラウザにアイドル時間に login-modal-chunk.js
ファイルをプリフェッチするように指示します。
プリロードディレクティブには、プリフェッチと比較して多くの違いがあります。
この例として、常に別のチャンクにあるべき大きなライブラリに依存するComponent
がある場合を考えます。
ChartingLibrary
という大きなライブラリを必要とするChartComponent
というコンポーネントを想像してみましょう。レンダリング時にLoadingIndicator
を表示し、オンデマンドでChartingLibrary
をインポートします。
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
ChartComponent
を使用するページがリクエストされると、<link rel="preload">
を介してcharting-library-chunkもリクエストされます。ページチャンクが小さく、より速く完了すると仮定すると、ページは、既にリクエストされたcharting-library-chunk
が完了するまで、LoadingIndicator
とともに表示されます。これにより、2往復ではなく1往復で済むため、ロード時間が少し短縮されます。特に高レイテンシー環境ではそうです。
場合によっては、プリロードを独自に制御する必要がある場合があります。たとえば、動的インポートのプリロードは、非同期スクリプトを介して実行できます。これは、ストリーミングサーバーサイドレンダリングの場合に役立ちます。
const lazyComp = () =>
import('DynamicComponent').catch((error) => {
// Do something with the error.
// For example, we can retry the request in case of any net error
});
webpack自体がそのスクリプトのロードを開始する前にスクリプトのロードが失敗した場合(webpackは、そのスクリプトがページ上にない場合、そのコードをロードするためのスクリプトタグを作成します)、そのcatchハンドラーはchunkLoadTimeoutが経過するまで開始されません。この動作は予想外の場合があります。しかし、これは説明可能です。webpackはエラーをスローできません。webpackは、スクリプトが失敗したことを認識していないためです。webpackは、エラーが発生した直後にスクリプトにonerrorハンドラーを追加します。
このような問題を回避するには、エラーが発生した場合にスクリプトを削除する独自のonerrorハンドラーを追加できます。
<script
src="https://example.com/dist/dynamicComponent.js"
async
onerror="this.remove()"
></script>
その場合、エラーになったスクリプトは削除されます。webpackは独自のスクリプトを作成し、エラーはタイムアウトなしで処理されます。
コードの分割を開始すると、モジュールがどこに配置されたかを確認するために、出力を分析することが役立つ場合があります。公式の分析ツールは、開始するのに最適な場所です。他にも、コミュニティでサポートされているオプションがいくつかあります。
遅延ロードでは、実際のアプリケーションでimport()
をどのように使用できるかについてより具体的な例を参照してください。また、キャッシュでは、コードをより効果的に分割する方法を学ぶことができます。