Webpack を使用してアプリを可能な限り小さくする方法
アプリケーションを最適化する際に最初に行うことの 1 つは、アプリケーションを可能な限り小さくすることです。webpack での手順は以下のとおりです。
本番環境モードを使用する(webpack 4 のみ)
Webpack 4 では、新しい mode
フラグが導入されました。このフラグを 'development'
または 'production'
に設定して、特定の環境用のアプリケーションをビルドしていることを Webpack に知らせます。
// webpack.config.js
module.exports = {
mode: 'production',
};
製品版アプリをビルドする際は、必ず production
モードを有効にしてください。これにより、webpack は圧縮やライブラリ内の開発専用コードの削除などの最適化を適用します。
関連情報
圧縮を有効にする
圧縮とは、余分なスペースを削除したり、変数名を短くしたりしてコードを圧縮することです。例:
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
↓
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}
Webpack は、バンドルレベルの圧縮とローダ固有のオプションという 2 つの方法でコードを圧縮できます。これらは同時に使用する必要があります。
バンドルレベルの圧縮
バンドルレベルの圧縮では、コンパイル後にバンドル全体が圧縮されます。仕組みは次のとおりです。
次のようなコードを記述します。
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack はこれを次のようにコンパイルします。
// bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); }
圧縮ツールは、おおよそ次の形式に圧縮されます。
// minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
webpack 4 では、バンドルレベルの圧縮が本番環境モードでも使用しないモードでも自動的に有効になります。内部で UglifyJS ミニファイアを使用しています。(圧縮を無効にする必要がある場合は、開発モードを使用するか、false
を optimization.minimize
オプションに渡します)。
webpack 3 では、UglifyJS プラグインを直接使用する必要があります。プラグインには webpack がバンドルされています。有効にするには、config の plugins
セクションに追加します。
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
ローダ固有のオプション
コードを圧縮する 2 つ目の方法は、ローダ固有のオプション(ローダの概要)です。ローダ オプションを使用すると、圧縮ツールでは圧縮できないデータを圧縮できます。たとえば、css-loader
を使用して CSS ファイルをインポートすると、ファイルは文字列にコンパイルされます。
/* comments.css */
.comment {
color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
このコードは文字列であるため、圧縮ツールで圧縮できません。ファイルの内容を圧縮するには、ローダを次のように構成する必要があります。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
関連情報
- UglifyJsPlugin のドキュメント
- その他の一般的な圧縮ツール: Babel Minify、Google Closure Compiler
NODE_ENV=production
を実行します。
フロントエンド サイズを小さくするもう 1 つの方法は、コードの NODE_ENV
環境変数を値 production
に設定することです。
ライブラリは NODE_ENV
変数を読み取り、開発モードまたは本番環境モードのうち、どのモードで動作すべきかを検出します。一部のライブラリは、この変数に基づいて動作が異なります。たとえば、NODE_ENV
が production
に設定されていない場合、Vue.js は追加のチェックを行い、警告を出力します。
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React も同じように動作します。警告が表示された開発ビルドを読み込みます。
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
通常、このようなチェックや警告は本番環境では不要ですが、コードには残るため、ライブラリのサイズが増加します。webpack 4 では、optimization.nodeEnv: 'production'
オプションを追加して削除します。
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
webpack 3 では、代わりに DefinePlugin
を使用します。
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
optimization.nodeEnv
オプションと DefinePlugin
はどちらも同じように機能します。つまり、すべての process.env.NODE_ENV
を指定された値に置き換えます。上記の構成を使用すると、次のようになります。
Webpack は、
process.env.NODE_ENV
をすべて"production"
に置き換えます。// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if (process.env.NODE_ENV !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
"production" !== 'production'
は常に false であり、プラグインはこれらのブランチ内のコードが実行されないことを認識するため、圧縮器はそのようなif
ブランチをすべて削除します。// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js (without minification) if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; }
関連情報
- 「環境変数」とは
DefinePlugin
、EnvironmentPlugin
に関する Webpack ドキュメント
ES モジュールを使用する
フロントエンド サイズを小さくする次の方法は、ES モジュールを使用することです。
ES モジュールを使用すると、webpack でツリーシェイキングが可能になります。ツリー シェイキングとは、バンドラが依存関係ツリー全体を走査し、使用されている依存関係をチェックして、使用されていない依存関係を削除することです。そのため、ES モジュール構文を使用すると、webpack は未使用のコードを削除できます。
複数の書き出しを含むファイルを作成しましたが、アプリはそのうちの 1 つのみを使用します。
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack は、
commentRestEndpoint
が使用されていないこと、およびバンドル内に個別のエクスポート ポイントを生成しないことを認識します。// bundle.js (part that corresponds to comments.js) (function(module, __webpack_exports__, __webpack_require__) { "use strict"; const render = () => { return 'Rendered!'; }; /* harmony export (immutable) */ __webpack_exports__["a"] = render; const commentRestEndpoint = '/rest/comments'; /* unused harmony export commentRestEndpoint */ })
圧縮器は、未使用の変数を削除します。
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
これは、ES モジュールで記述されたライブラリでも機能します。
ただし、webpack に組み込まれている圧縮機能(UglifyJsPlugin
)をそのまま使用する必要はありません。デッドコードの削除をサポートする圧縮ツール(Babel Minify プラグイン、Google Closure Compiler プラグインなど)を使用できます。
関連情報
ツリー シェイキングに関する Webpack ドキュメント
画像を最適化する
画像はページサイズの半分以上を占めます。これらは JavaScript ほど重要ではありませんが(たとえば、レンダリングをブロックしません)、帯域幅の大部分を消費します。url-loader
、svg-url-loader
、image-webpack-loader
を使用して webpack で最適化します。
url-loader
は、小さな静的ファイルをアプリにインライン化します。構成しない場合、渡されたファイルを受け取り、コンパイル済みバンドルの隣に配置して、そのファイルの URL を返します。ただし、limit
オプションを指定すると、この上限より小さいファイルが Base64 データ URL としてエンコードされ、この URL が返されます。これにより、画像が JavaScript コードにインライン化され、HTTP リクエストが保存されます。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loader
は url-loader
と同様に機能しますが、Base64 ではなく URL エンコードでファイルをエンコードします。これは SVG 画像の場合に有用です。SVG ファイルは単なる書式なしテキストであるため、このエンコードの方がサイズ効率に優れています。
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
は通過する画像を圧縮します。JPG、PNG、GIF、SVG 画像をサポートしているため、これらのすべてのタイプにこのモードを使用します。
このローダーはアプリに画像を埋め込みないため、url-loader
および svg-url-loader
とペアで機能する必要があります。両方のルール(JPG/PNG/GIF 画像用のルールと SVG 画像用のルール)にコピー&ペーストすることを避けるため、このローダは enforce: 'pre'
を使用して別のルールとして含めます。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
このローダーのデフォルト設定はこれでそのままになっていますが、さらに設定する場合は、プラグインのオプションをご覧ください。指定するオプションを選択するには、Addy Osmani による画像最適化に関する優れたガイドをご覧ください。
関連情報
- 「base64 エンコードは何に使用されますか?」
- Addy Osmani による画像の最適化に関するガイド
依存関係を最適化する
JavaScript の平均サイズの半分以上は依存関係に起因しており、そのサイズの一部は単に不要な場合があります。
たとえば、Lodash(v4.17.4 の時点で)は、72 KB の圧縮されたコードをバンドルに追加します。しかし、そのメソッドを 20 個などだけ使用する場合、約 65 KB の圧縮されたコードでは何も起こりません。
もう一つの例は Moment.js です。バージョン 2.19.1 の圧縮コードは 223 KB と非常に大きく、1 ページあたりの JavaScript の平均サイズは 2017 年 10 月の 452 KB でした。ただし、そのサイズの 170 KB はローカライズ ファイルです。複数の言語で Moment.js を使用しない場合、これらのファイルは目的もなくバンドルを肥大化させます。
これらの依存関係はすべて簡単に最適化できます。最適化のアプローチは GitHub リポジトリにまとめていますので、こちらをご覧ください。
ES モジュールのモジュールの連結(スコープ ホイスティング)を有効にする
バンドルのビルド時に、webpack は各モジュールを関数にラップします。
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
})
以前は、CommonJS と AMD のモジュールを互いに分離するために、この処理が必要でした。ただし、これにより各モジュールのサイズとパフォーマンスのオーバーヘッドが増加しました。
Webpack 2 では、CommonJS や AMD モジュールとは異なり、それぞれを関数でラップせずにバンドルできる ES モジュールのサポートが導入されました。webpack 3 では、モジュールの連結によってそのようなバンドルが可能になりました。モジュールの連結による処理は次のとおりです。
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
違いをご確認ください。プレーン バンドルでは、モジュール 0 はモジュール 1 の render
を必要としていました。モジュールの連結では、require
は単に必要な関数に置き換えられ、モジュール 1 は削除されます。バンドルの方がモジュール数が少なく、モジュールのオーバーヘッドも少なくなります。
この動作を有効にするには、Webpack 4 で optimization.concatenateModules
オプションを有効にします。
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
webpack 3 では、ModuleConcatenationPlugin
を使用します。
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
関連情報
- ModuleConcatenationPlugin 用 Webpack ドキュメント
- 「スコープ ホイスティングの簡単な概要」
- このプラグインの機能の詳細な説明
webpack コードと非 Webpack コードの両方がある場合は、externals
を使用します。
大規模なプロジェクトで、Webpack を使用してコンパイルするコードとそうでないコードがある場合があります。たとえば、動画ホスティング サイトの場合、プレーヤー ウィジェットは Webpack を使用して構築されていて、周囲のページは以下のようになっている場合があります。
両方のコードに共通の依存関係がある場合は、コードを共有することで、コードが複数回ダウンロードされないようにできます。これを行うには webpack の externals
オプションを使用します。これは、モジュールを変数やその他の外部インポートに置き換えます。
window
で依存関係を利用できるかどうか
Webpack 以外のコードが window
で変数として使用可能な依存関係に依存している場合は、依存関係名を変数名にエイリアスします。
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
この設定では、webpack は react
パッケージと react-dom
パッケージをバンドルしません。代わりに、次のように置き換えられます。
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
依存関係が AMD パッケージとして読み込まれるかどうか
Webpack 以外のコードで window
に依存関係を公開しない場合、状況はより複雑になります。ただし、webpack 以外のコードがこれらの依存関係を AMD パッケージとして使用している場合は、同じコードを 2 回読み込むことを避けることができます。
そのためには、Webpack コードを AMD バンドルとしてコンパイルし、モジュールをライブラリ URL にエイリアスします。
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack はバンドルを define()
にラップし、次の URL に依存するようにします。
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
Webpack 以外のコードが同じ URL を使用して依存関係を読み込む場合、これらのファイルは 1 回だけ読み込まれます。それ以降のリクエストではローダ キャッシュが使用されます。
関連情報
externals
の Webpack ドキュメント
まとめ
- webpack 4 を使用する場合は本番環境モードを有効にする
- バンドルレベルのミニファイアとローダのオプションを使用してコードを最小化する
NODE_ENV
をproduction
に置き換えて、開発専用のコードを削除します。- ES モジュールを使用してツリー シェイキングを有効にする
- 画像を圧縮する
- 依存関係固有の最適化を適用する
- モジュールの連結を有効にする
- 必要に応じて
externals
を使用してください