LoginSignup
49
58

More than 5 years have passed since last update.

webpack2 でのコード分割 (Code Splitting) について調べる

Last updated at Posted at 2017-05-03

code splittingを頑張らずに切り抜けられるかと思ったけど、まだちょっと逃げ切れなそうだったのでちゃんと調べた。
Chrome Canaryに<script module> が来てたり数年スパンだと不要になりそうな技術だけど、もう少しの間はこのへんを扱っていかないといけなそう)

Code splitting

まず普通のコード

こんなコードを用意する

// index.js
const lib = require("./lib")

lib()
// lib.js
module.exports = () => {
  console.log("This is Lib")
}

するとこんなふうにビルドされる

// モジュール解決する基本的な部分が差し込まれる
// ...
// ...
/******/    function __webpack_require__(moduleId) {
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/    }
// ...
// ...
// それが終わると、自分が書いたモジュールが差し込まれる

/******/ ([
/* 0 */
/***/ (function(module, exports) {

module.exports = () => {
  console.log("This is Lib")
}


/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

const lib = __webpack_require__(0)

lib()

/***/ })
/******/ ]);

__webpack_require__によってrequireが解決される様子が見て取れる。

__webpack_require__の詳細は、lib/MainTemplate.jsあたりで構築されている(文字列でjavascriptが組み立てられているっぽい。つらそう)

webpackランタイム部分の分離(Manifest分離)

この__webpack_require__に関するランタイム部分をCommonChunkPluginを利用することで分離することが紹介されている。

webpack公式ではこれをManifest fileと呼称している。
railsあたりと組み合わせた時のmanifestと役割が似ているが、微妙に違う

// webpack.config.js
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({ 
      name: 'manifest',
      minChunks: Infinity
    })
  ]

CommonChunkPluginで分離されたmanifestには、webpack内部でのモジュール解決に利用される関数のみが抽出される。
非同期ローディング用の関数(requireEnsure)も含まれるようになる。

// manifest.js
 (function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
    var parentJsonpFunction = window["webpackJsonp"];
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // manifest外に書かれたscriptをJSONPとして実行する。
    // ...
    };

    // webpack内部のrequire関数
    function __webpack_require__(moduleId) {
    // ...
    // ...
    }

  // requireEnsure。関数を非同期ロードする仕組み。
    __webpack_require__.e = function requireEnsure(chunkId) {
    // ... 
    // ...

        // コード読み込みは、<script>タグを生成してDOMに埋め込んで読み込む
        var head = document.getElementsByTagName('head')[0];
        var script = document.createElement('script');
        script.type = 'text/javascript';
    // ...
    // ...
        // 後述する非同期読み込みを行うと、それらの読み込みをうまく解決するコードが差し込まれる。manifestっぽさのある部分
        script.src = __webpack_require__.p + "" + ({"1":"main"}[chunkId]||chunkId) + ".js?" + {"0":"d5186cef317c5c895373","1":"eb51fb214d292368d33e"}[chunkId] + "";

        head.appendChild(script);

        return promise;
    };
  // ここから下は様々便利関数などが展開される
  // ...
  // ...

 })
 ([]);

残されたメインのコード側は、webpackJsonpで囲まれる。
これがmanifest.jsによってうまく読み込み解決される

// main.js

webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {

module.exports = () => {
  console.log("This is Lib")
}

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

const lib = __webpack_require__(0)

lib()

/***/ })
],[1]);

import() / require.ensure / bundle でLazy importする

非同期処理するのは現在webpackは3つほどやり方が提供されている。

import()

import()Dynamic importと呼ばれる現在stage-3の機能。
非同期をPromise読み込みする。一番記述量が少なく書ける。

const main = () => {
  const lib = import("./lib")
  lib()
}

// main.js
webpackJsonp([1],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const main = () => {
  const lib = __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1))
  lib()
}


/***/ })
],[0]);

webpackでは内部的にrequireEnsure(__webpack_require__.e)が利用される

require.ensure

webpack1時代から存在するwebpack固有のrequire拡張。import()より少し機能が多いが、import()に置き換えられるということがアナウンスされている。

const main = () => {
  require.ensure([], (require) => {
    const lib = require("./lib")
    lib()
  })
}
webpackJsonp([1],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const main = () => {
  __webpack_require__.e/* require.ensure */(0).then(((require) => {
    const lib = __webpack_require__(1)
    lib()
  }).bind(null, __webpack_require__)).catch(__webpack_require__.oe)
}


/***/ })
],[0]);

bundle-loader

require-ensureの別解としてbundle-loaderも存在する。
こちらもおそらくimport()を利用すれば使う事は無いと思われるが、比較として掲載する

const main = () => {
  const lib = require("bundle-loader?lazy!./lib")
  lib()
}
// main.js
webpackJsonp([1],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

module.exports = function(cb) {
    __webpack_require__.e/* require.ensure */(0).then((function(require) {
        cb(__webpack_require__(2));
    }).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
}

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

const main = () => {
  const lib = __webpack_require__(0)
  lib()
}


/***/ })
],[1]);

System.import

System.import is deprecated だそうです
https://webpack.js.org/guides/code-splitting-async/#system-import-is-deprecated
さして難しくも無いはずなので、import()へ置き換えるだけでよさそう

import()についてもうちょっと深いトピック

webpackでのimport()の制限

なんとなくimport()の構文を見ると

const lazy = (modulePath) => import(modulePath)

的なことが出来そうな気がしてしまうが、これは下記のようなWarningが出る

10:9-22 Critical dependency: the request of a dependency is an expression

webpackにおいては、import()をビルド時に静的解決するので、これが出来ないような作りになっている。

実際のビルド結果では、不正なパッケージとして扱われ、その実態はwebpackEmptyContextに置き換えられてしまう。

var lazySomeApp = lazy("./SomeApp");

var lazy = path = __webpack_require__(281)(path);

/***/ 281:
/***/ (function(module, exports) {

function webpackEmptyContext(req) {
    throw new Error("Cannot find module '" + req + "'.");
}
webpackEmptyContext.keys = function() { return []; };
webpackEmptyContext.resolve = webpackEmptyContext;
module.exports = webpackEmptyContext;
webpackEmptyContext.id = 281;

/***/ }),

babel-plugin-syntax-dynamic-import

babelのbabel-plugin-syntax-dynamic-importを使うやり方も記載されている。

webpack側では解決されず、ただのpromiseに変換される。ファイル分割もされなくなる。
「import()は使いたいけどコード分離はしたくない」とかの場合には使えるかもしれない。

// babelがimport()をこんな感じの関数に置き換える。ただのpromiseになる
const importConverted = (module) => Promise.resolve(module)

TypeScriptとの組み合わせ

TypeScriptと組み合わせた時、現状だと構文エラー扱いになってしまう。
https://github.com/Microsoft/TypeScript/issues/12364

import()を利用する箇所だけjsファイル化する妥協が必要になる。型がつかない問題もあるものの、そのへんはd.tsの型定義ファイルを別で作るか、2.3以上から対応されたJSDocでカバーすると良さそう。

// import.js
export const lazySomeApp = () => import("./SomeApp")
// import.d.ts
import * as SomeApp from "./SomeApp"

export const lazySomeApp = () => Promise<typeof SomeApp>
// Typescript 2.3以降でのJSDoc利用
/**
 * @template T
 * @return {Promise<T>}
 */
export const lazySomeApp = () => import("./SomeApp")

その他にもissueでは、_import()でちょろまかす手段も提案されている。かなりトリッキーかもしれない。
https://github.com/Microsoft/TypeScript/issues/12364#issuecomment-270819757

declare function _import<T>(path: string): Promise<T>;
const lazyPromise = _import("./SomeApp")
// webpack.config.js
// webpack側でこれを置き換えている。力技感
{
    loader: 'string-replace-loader',
    options: {
        search: '_import(',
        replace: 'import('
    }
},

reactとの組み合わせ

非同期なcomponentをreactで扱おうとすると一工夫する必要が出て来る。
そんなに難しいものではないのでreact-routerのcode splittingページあたりを参考に作るでも良いが、ざっくり調べるとこのへんがあった

  • async-reactor
    • Star: 200 over
    • async/awaitをベースにしたcomponentローダー
    • コードベースが小さく素直な作りに見える
  • react-async-component
    • Star: 300 over
    • ベータ扱い
    • SSRも対応している
  • react-loadable
    • Star: 2000 over
    • Issueが閉じられてPull Requ受付のみになっている。
    • 内部で色々やっている

react-loadableは良いのだがissueを受け付けてないという点に少々怖さもあり、個人的に利用するならasync-reactorかreact-async-componentが良さそうだなと感じている。

(おまけ)更なるCode splittingについても少し調べる

ちょっと実戦投入するには勇気がいりそうなものを触りだけ。

SWPrecacheWebpackPlugin

service-workerのPrecacheをする

const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
// ...
// ...
// ...
plugins: [
  new SWPrecacheWebpackPlugin({
    cacheId: 'dynamic-example',
  })

これを実行すると、service-worker.jsというファイルが出力される。
これを下記のように読み込むことでservice-workerが有効になる

(function() {
  if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
  }
})();

react-routerのwebsiteでも使われているが、現在はコメントアウトされている

AggressiveSplittingPlugin

読んで字の如くアグレッシブに分割するplugin。特性上、ほぼHTTP2向けという謳い文句。

作者による記事もある。
https://medium.com/webpack/webpack-http-2-7083ec3f3ce6

plugins: [
  new webpack.optimize.AggressiveSplittingPlugin({
    minSize: 30000,
    maxSize: 50000
  }),
]

指定されたminSize以上maxSize未満を目指して分離していく。
reactあたりをimportしてみるとわかりやすく分割される

const react = require("react")
const main = () => {
  console.log("This is main")
  console.log(react)
}

0,1,2に分割される

      Asset     Size  Chunks             Chunk Names
       0.js  50.4 kB       0  [emitted]
       1.js    52 kB       1  [emitted]
       2.js  42.4 kB       2  [emitted]

__webpack_require__などの挙動、コードベースは特に変更されない。
また、こちらは特にlazyなloadingを目指したものではないので、<script>タグで全部読み込むのが前提に鳴る

<script src="0.js"></script>
<script src="1.js"></script>
<script src="2.js"></script>
49
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
58