30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

webpack@4の splitChunks を使って CommonsChunkPlugin から移行する

この記事では、webpackのv4で追加されたsplitChunksを使って、v4で削除されたCommonsChunkPluginから移行する方法を書いていく。

使っているパッケージのバージョンは以下の通り。

  • webpack@4.1.0
  • webpack-cli@2.0.10

CommonsChunkPlugin とは

CommonsChunkPluginの説明やそれを使うメリットは、以下の記事が分かりやすい。

webpackのCommonsChunkPluginの使い方、使い所 (webpack 4で廃止) - Qiita

簡単に言ってしまうと、複数のエントリポイントで共通のライブラリを使っている場合、それぞれのファイルに個別にバンドルするのではなく、そのライブラリだけ別のファイルとして出力する。
そうすることで、全体のファイルサイズが小さくなる、キャッシュを活用しやすい、といったメリットを得られる。

例えば、以下のような場合。

entry: {
  home: './src/home.js',
  about: './src/about.js',
  error: './src/error.js',
},
output: {
  filename: '[name].js',
  path: path.resolve(__dirname, 'dest'),
},

この設定でビルドすると、dest/home.jsdest/about.jsdest/error.jsの3つのファイルが出力される。
このうち、homeでもaboutでもReactを使っていた場合、dest/home.jsdest/about.jsの両方にReactがバンドルされ、重複が発生してしまう。
共通して使っているライブラリが増えたり、エントリポイントが増えたりしていくほど、重複によるムダも大きくなっていく。

CommonsChunkPluginを使うことで、この状態を回避できる。
例えば、共通して使っているReactはdest/vendor.jsというファイルにバンドルしてしまうことで、dest/home.jsdest/about.jsにReactがバンドルされることを防げる。

しかしwebpackのv4でCommonsChunkPluginは削除されてしまった。
v4で同じことをするためには、splitChunksを使う。

ライブラリの重複

具体的な使い方を示すために、まずはsplitChunksを使わずにビルドしてみる。

エントリポイントは先程の例と同じで、それ以外に何も設定しない。

webpack.config.js

const path = require('path');

module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js',
    error: './src/error.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dest'),
  },
};

エントリポイントのファイルの内容は、それぞれ以下の通り。
JSXを使っていないのは、BabelやらLoaderやらを使って設定項目が増えて分かりづらくなるのを避けるためで、深い意味はない。

src/home.js

import React from 'react';
import ReactDOM from 'react-dom';

console.log('This is Home.');

function Home() {
  return React.createElement(
    'div',
    null,
    'home'
  );
}

ReactDOM.render(React.createElement(Home, null), document.querySelector('#app'));

src/about.js

import React from 'react';
import ReactDOM from 'react-dom';

console.log('This is About.');

function About() {
  return React.createElement(
    'div',
    null,
    'about'
  );
}

ReactDOM.render(React.createElement(About, null), document.querySelector('#app'));

src/error.js

console.log('This is Error.');

homeaboutの両方で、ReactとReactDOMを使っている。

この状態でビルドし、出力されたファイルをそれぞれ、以下のようなhtmlファイルで読み込む。

<body>
    <div id="app"></div>
    <script src="./home.js"></script>
</body>

ディレクトリ構成は以下。

├── dest
│   ├── about.html
│   ├── about.js
│   ├── error.html
│   ├── error.js
│   ├── home.html
│   └── home.js
├── package-lock.json
├── package.json
├── src
│   ├── about.js
│   ├── error.js
│   └── home.js
└── webpack.config.js

それぞれのhtmlファイルをブラウザで開くと、きちんと動いていることが分かる。
だが、dest/homs.jsdest/about.jsの両方にReactとReactDOMがバンドルされてしまい、ファイルサイズが大きくなってしまっている。

   Asset       Size  Chunks             Chunk Names
error.js  578 bytes       0  [emitted]  error
about.js   96.9 KiB       1  [emitted]  about
 home.js   96.9 KiB       2  [emitted]  home

splitChunks を使って重複を解消する

splitChunksを使うには、webpack.config.jsoptimization.splitChunksを追加する。

const path = require('path');

module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js',
    error: './src/error.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dest'),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /react|react-dom/,
          name: "vendor",
          chunks: "initial",
          enforce: true,
        },
      },
    },
  },
};

上記の設定だと、reactreact-domdest/vendor.jsにバンドルされて出力され、dest/home.jsdest/about.jsには含まれなくなる。

その結果、全体のファイルサイズが小さくなっていることが分かる。

    Asset       Size  Chunks             Chunk Names
vendor.js   93.1 KiB       0  [emitted]  vendor
 error.js  578 bytes       1  [emitted]  error
 about.js   4.46 KiB       2  [emitted]  about
  home.js   4.46 KiB       3  [emitted]  home

それぞれのhtmlでdest/vendor.jsも読み込むようにすることを忘れずに行う。

<body>
    <div id="app"></div>
    <script src="./vendor.js"></script>
    <script src="./home.js"></script>
</body>
<body>
    <div id="app"></div>
    <script src="./vendor.js"></script>
    <script src="./about.js"></script>
</body>

どちらのページでも同じdest/vendor.jsを読み込んでいるからキャッシュが効くし、ライブラリをまとめたvendor.jsの更新頻度はhome.jsabout.jsよりも多くないはずなので、そういった意味でもキャッシュの恩恵を受けやすい。

参考資料