何が分割パッケージか#
分割パッケージ(Code Splitting)は、アプリケーションのコードを単一のファイルにまとめるのではなく、複数の小さなパッケージに分割するための最適化技術です。この技術は、特に大規模なアプリケーションにおいて、アプリケーションのパフォーマンスと読み込み速度を大幅に向上させることができます。
Webpack における分割パッケージ#
Webpack は、初期化、コンパイル、最適化、出力、プラグインの実行などの複数の段階を経て、ソースコードをパッケージ(Chunk)として出力します。Webpack における分割パッケージの動作は、Webpack4 からデフォルトの動作となり、設定ファイルではoptimization.splitchunks
というオブジェクトリテラルを通じて関連する動作を設定できます (https://webpack.js.org/plugins/split-chunks-plugin/)。
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
// node_modulesからインポートされた依存関係を分割
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
// 公共モジュールを分割
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
分割パッケージ戦略#
分割パッケージの大まかな考え方には以下の 2 つがあります。
- 動的インポート(dynamic import)
- 大きなファイルを分割し、小さなファイルと公共モジュールを統合する
動的インポート#
動的インポートは遅延読み込みとも言え、必要でないときにはモジュールファイルを取得せず、使用時にのみサーバーから関連ファイルを取得します。Js に組み込まれたimport()
文、React のlazy
メソッド、そして vue-router の即使用可能なルート遅延読み込みはすべて動的読み込みの実装です。
パッケージの分割と統合#
- Webpack は出力時に設定された filename に基づいて各 Chunk にハッシュを追加し、変更されないモジュールを適切に分割することで、ファイルはクライアントに長期間キャッシュされ、複数回のパッケージデプロイにおいてもキャッシュが有効です。
- ネットワークリソースに基づいて、大きな Chunk を複数の小さな Chunk に分割することで、アプリケーションの読み込みがより効率的になります。例えば、http2 では、この操作がネットワークリソースの利用率をさらに向上させます。
- いくつかの Chunk の公共部分を抽出し、モジュールの重複注入を避けます。
実践#
- cdn とキャッシュグループの比較
cdn からのパッケージ内容のインポートは、実際には長期的なキャッシュを実現する方法の一つですが、tree-shaking の広範な適用に伴い、cdn はある意味で負の最適化になってしまいました。
1. cdnインポート
//webpack.config.js
modules.exports={
externals: {
lodash: '_'
}
}
//index.html
//externalsを使用してlodashを除外し、テンプレートファイル内でscriptを通じて読み込むことで、実際にはlodashライブラリ全体を直接取得しています。
<script src="bootcdn/ajax/[email protected]/lodash.min.js">
2. キャッシュグループの設定
modules.exports={
optimization.splitchunks: {
chunks: "all",
cacheGroups: {
// 最後の最適化段階で、未使用のlodashメソッドを削除します。cdnインポートと比較して、必要なコード量は明らかに少なくなります。
lodash: {
test: [\\/]node_modules[\\/]lodash,
}
}
}
}
ただし、特定のシナリオでは cdn が依然として有用です。例えば、サーバーが http1.1 を使用してデータを転送し、依存関係が小さいか全量使用される場合です。
- 変更されないモジュールの合理的な分割
Webpack4 のデフォルトの分割動作は、node_modules からインポートされた依存関係をすべて 1 つの Chunk にパッケージ化します。その中には、頻繁に変更される依存関係、例えば社内の sdk やコンポーネントライブラリはデフォルトの分割戦略を採用できますが、長期間変更のない依存関係はキャッシュグループを使用して分割し、ブラウザに長期間キャッシュさせることができます。
modules.exports={
optimization.splitchunks: {
chunks: "all",
cacheGroups: {
vue: {
test: [\\/]node_modules[\\/](vue|vue-router|vuex),
},
lodash: {
test: [\\/]node_modules[\\/]lodash
},
element: {
test: [\\/]node_modules[\\/]element-ui
}
}
}
}
- 遅延読み込み
Js に組み込まれた import () メソッドを使用して、コンポーネントを動的にインポートします。
export default {
component: () => import('./A.vue'),
}
同様に、ルートコンポーネントを動的にインポートします。
const router = createRouter({
// ...
routes: [
{ path: '/a', component: () => import('./views/A.vue') },
]
})
いくつかのフレームワークに組み込まれた遅延読み込みメソッド、例えば react の lazy。
import { useState, Suspense, lazy } from 'react';
import Loading from './Loading.js';
const MarkdownPreview = lazy(() => delayForDemo(import('./MarkdownPreview.js')));
export default function MarkdownEditor() {
const [showPreview, setShowPreview] = useState(false);
const [markdown, setMarkdown] = useState('こんにちは、**世界**!');
return (
<>
<textarea value={markdown} onChange={e => setMarkdown(e.target.value)} />
<label>
<input type="checkbox" checked={showPreview} onChange={e => setShowPreview(e.target.checked)} />
プレビューを表示
</label>
<hr />
{showPreview && (
<Suspense fallback={<Loading />}>
<h2>プレビュー</h2>
<MarkdownPreview markdown={markdown} />
</Suspense>
)}
</>
);
}
function delayForDemo(promise) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
}).then(() => promise);
}