Webpack の最適化ボトルネックは、主に 2 つの側面があります:
- Webpack のビルドプロセスが非常に時間がかかる
- Webpack がパッケージ化した結果のサイズが大きすぎる
ビルドプロセスの高速化戦略#
loader に多くのことをさせない ——babel-loader の例#
最も一般的な最適化方法は、include または exclude を使用して不要なトランスパイルを避けることです。たとえば、Webpack 公式が babel-loader を紹介する際に示した例:
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
このコードは、巨大な node_modules フォルダーや bower_components フォルダーの処理を回避します。しかし、ファイル範囲を制限することによるパフォーマンス向上には限界があります。それに加えて、キャッシュを有効にしてトランスパイル結果をファイルシステムにキャッシュすることで、少なくとも babel-loader の作業効率を 2 倍に向上させることができます。これを実現するには、loader に適切なパラメータ設定を追加するだけです:
loader: 'babel-loader?cacheDirectory=true'
サードパーティライブラリを見逃さない#
サードパーティライブラリを処理する方法は多くありますが、CommonsChunkPlugin は毎回のビルドで vendor を再構築します。効率を考慮して、私たちはより多くの場合 DllPlugin を使用します。
DllPlugin は Windows のダイナミックリンクライブラリ(dll)の考えに基づいて作成されました。このプラグインは、サードパーティライブラリを別のファイルにパッケージ化します。このファイルは単なる依存ライブラリです。この依存ライブラリは、ビジネスコードと一緒に再パッケージ化されることはなく、依存関係自体のバージョンが変更されたときのみ再パッケージ化されます。
DllPlugin を使用してファイルを処理するには、2 つのステップを踏む必要があります:
- dll 専用の設定ファイルに基づいて dll ライブラリをパッケージ化する
- webpack.config.js ファイルに基づいてビジネスコードをパッケージ化する
React に基づくシンプルなプロジェクトの例として、dll の設定ファイルは次のように記述できます:
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 依存のライブラリ配列
vendor: [
'prop-types',
'babel-polyfill',
'react',
'react-dom',
'react-router-dom',
]
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
// DllPluginのname属性はlibaryと一致する必要があります
name: '[name]_[hash]',
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
// contextはwebpack.config.jsと一致する必要があります
context: __dirname,
}),
],
}
作成が完了したら、この設定ファイルを実行します。dist フォルダーには、各サードパーティライブラリに対応する具体的なパスを記述した vendor-manifest.json というファイルが生成されます。
その後、webpack.config.js で dll に対して少し設定を行うだけです:
const path = require('path');
const webpack = require('webpack')
module.exports = {
mode: 'production',
// コンパイルエントリ
entry: {
main: './src/index.js'
},
// 目標ファイル
output: {
path: path.join(__dirname, 'dist/'),
filename: '[name].js'
},
// dll関連の設定
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifestは最初のステップでパッケージ化されたjsonファイルです
manifest: require('./dist/vendor-manifest.json'),
})
]
}
Happypack——loader を単一プロセスからマルチプロセスに変更#
皆さんご存知のように、Webpack はシングルスレッドです。たとえ複数のタスクが存在しても、1 つずつ順番に処理を待たなければなりません。これは Webpack の欠点ですが、幸い私たちの CPU はマルチコアです。Happypack は CPU のマルチコア並列処理の利点を最大限に活用し、タスクを複数の子プロセスに分解して並行実行することで、パッケージ化の効率を大幅に向上させます。
HappyPack の使用方法も非常に簡単です。loader の設定を HappyPack に移すだけで、必要な並行プロセスの数を手動で HappyPack に伝えることができます:
const HappyPack = require('happypack')
// 手動でプロセスプールを作成
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
...
{
test: /\.js$/,
// クエリパラメータはこのファイルを処理するHappyPackインスタンスの名前を指定します
loader: 'happypack/loader?id=happyBabel',
...
},
],
},
plugins: [
...
new HappyPack({
// このHappyPackの「名前」はhappyBabelで、上記のクエリパラメータと呼応しています
id: 'happyBabel',
// プロセスプールを指定
threadPool: happyThreadPool,
loaders: ['babel-loader?cacheDirectory']
})
],
}
ビルド結果のサイズ圧縮#
ファイル構造の可視化、サイズが大きすぎる原因を特定#
非常に便利なパッケージ構成の可視化ツール ——webpack-bundle-analyzer。設定方法は通常の plugin と同じで、矩形ツリー図の形式でパッケージ内の各モジュールのサイズと依存関係を表示します。使用する際は、プラグインの形式でインポートするだけです:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
冗長コードの削除#
import/export 構文に基づいて、Tree-Shaking はコンパイルプロセス中にどのモジュールが実際に使用されていないかを把握できます。これらの不要なコードは、最終的なパッケージ化の際に削除されます。Tree-Shaking は非常に特定的で、モジュールレベルの冗長コードを処理するのに適しています。より細かい粒度の冗長コードの削除は、通常 JS または CSS の圧縮または分離プロセスに統合されます。
ここでは、現在広く受け入れられている UglifyJsPlugin を例に、圧縮プロセス中に断片化された冗長コード(console 文、コメントなど)を自動的に削除する方法を見てみましょう:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyJsPlugin({
// 並行処理を許可
parallel: true,
// キャッシュを有効に
cache: true,
compress: {
// すべてのconsole文を削除
drop_console: true,
// 使用回数の多い静的値を自動的に変数として定義
reduce_vars: true,
},
output: {
// コメントを保持しない
comment: false,
// 出力コードをできるだけコンパクトに
beautify: false
}
})
]
}
この手動で UglifyJsPlugin をインポートするコードは実際には webpack3 の使い方で、webpack4 ではすでに uglifyjs-webpack-plugin をデフォルトで使用してコードを圧縮しています ——webpack4 では、optimization.minimize と optimization.minimizer を設定することで圧縮関連の操作をカスタマイズします。
必要に応じたロード#
- 一度にすべてのファイル内容をロードするのではなく、現在必要な部分のみをロードします(事前に分割を行います)
- さらに多くの内容が必要な場合、必要な内容を即時にロードします
必要に応じたロードを行わない場合、私たちのコードは次のようになります:
import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>
必要に応じたロードを有効にするには、少し変更が必要です。まず、webpack の設定ファイルを動かす必要があります:
output: {
path: path.join(__dirname, '/../dist'),
filename: 'app.js',
publicPath: defaultSettings.publicPath,
// chunkFilenameを指定
chunkFilename: '[name].[chunkhash:5].chunk.js',
},
ルーティングのコードも少し調整が必要です:
const getComponent => (location, cb) {
// コアメソッド
require.ensure([], (require) => {
cb(null, require('../pages/BugComponent').default)
}, 'bug')
},
...
<Route path="/bug" getComponent={getComponent}>
require.ensure(dependencies, callback, chunkName)
は非同期メソッドで、Webpack はパッケージ化時に BugComponent を別のファイルとして作成します。私たちが bug というルートに移動する際に、この非同期メソッドのコールバックが実行され、実際に BugComponent の内容を取得します。これが必要に応じたロードです。必要に応じたロードとは、基本的に正しいタイミングで適切なコールバックをトリガーすることです。