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>