JavaScriptには、import * as
という構文があります。これは、インポート先のモジュールの中身全部をオブジェクト(モジュール名前空間オブジェクト)として取得できる構文です。
import * as mod from "./some-module";
console.log(mod.foo, mod.bar);
たまに、「この構文を使うとTree Shakingが効かなくなる」といった説明が見られることがありますが、必ずしもそうではありません。そこで、この記事ではimport * as
構文とパフォーマンス最適化に関連する正しい知識と、その背景をご紹介します。
webpackで検証してみよう
Tree shakingを行うのはモジュールバンドラであることが知られています。そこで、webpackを使って色々と構文を検証してみましょう。今回は次のような設定を用います。これは最適化を切って出力ファイルを見やすくしつつ、import/export周りの最適化だけは有効にするという設定です。なお、この記事では執筆時点での最新バージョンである5.72.1を使用しています。
module.exports = {
mode: "none",
optimization: {
providedExports: true,
usedExports: true,
mangleExports: "deterministic",
},
};
最小構成で検証してみる
では、import * as
構文に対してwebpackがどう振る舞うのか最小構成で検証してみましょう。
export const foo = "foo";
export const bar = "bar";
import * as mod from "./mod";
console.log(mod.foo);
出力結果全体は長いので畳んでおきます。
dist/main.js
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "R": () => (/* binding */ foo)
/* harmony export */ });
/* unused harmony export bar */
const foo = "foo";
const bar = "bar";
/***/ })
/******/ ]);
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/* harmony import */ var _mod__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_mod__WEBPACK_IMPORTED_MODULE_0__/* .foo */ .R);
})();
/******/ })()
;
webpackの出力結果
src/mod.js
に相当する出力は次のようになっています。
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "R": () => (/* binding */ foo)
/* harmony export */ });
/* unused harmony export bar */
const foo = "foo";
const bar = "bar";
ポイントとして、foo
が外向きにはR
という名前でエクスポートされています。また、bar
が使われていないことも検知されています。
src/index.js
に相当する出力は次のようになっています。
/* harmony import */ var _mod__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_mod__WEBPACK_IMPORTED_MODULE_0__/* .foo */ .R);
このように、mod.foo
が _mod__WEBPACK_IMPORTED_MODULE_0__/* .foo */ .R
と変換されています。src/mod.js
側でfoo
がR
にリネームされたことに対応して、それを使う側もR
になっています。
このことから分かることは、import * as
構文を使ってもtree shakingが効くし、export名のmanglingも行われるということです。manglingというのは、(JavaScriptの文脈では)変数名などを短く書き直してコードサイズを減らすことを意味します。今回の場合foo
がR
になっています。
ちなみに、import { foo } from "./mod"
のようにnamed importを行なった場合もfoo
をR
にしてもらえます。import * as
構文もnamed importと同等のサポートを受けられるということですね。
なお、const foo
というように変数名がmanglingされていなかったりconst bar
が残っているのが気になるかもしれませんが、これは問題ありません。なぜなら、このあたりのmanglingや消去を担当するのはwebpackではなくminifier (terserなど)の役目だからです。
最適化が効かない場合
実のところ、import * as
構文を使うと最適化が効かない場合というのも存在します。次の場合がそうです。
export const foo = "foo";
export const bar = "bar";
import * as mod from "./mod";
- console.log(mod.foo);
+ console.log(mod);
webpackの出力結果
上の入力に対しては、webpackは次のような結果を出力します。
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "bar": () => (/* binding */ bar),
/* harmony export */ "foo": () => (/* binding */ foo)
/* harmony export */ });
const foo = "foo";
const bar = "bar";
/* harmony import */ var _mod__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_mod__WEBPACK_IMPORTED_MODULE_0__);
見て分かるように、foo
がR
に変わるというようなmanglingが行われていません。また、foo
もbar
も消されていません。
webpackはコードの意味を変えない
以上のような結果は、webpackはコードの意味を変えないという原則があると考えれば理解できます。今回の例ではconsole.log(mod)
では{ "foo": "foo", "bar": "bar" }
という結果になることが期待されます。この結果を維持するために、エクスポートされる名前を変えたり減らしたりすることはできませんでした。また、このようにmod
を直接参照されると、webpackのあずかり知らぬところでのちのちbar
を使われたりする可能性を捨て切れません。そのため、tree shakingが行えなくなります。
一方で、console.log(mod.foo)
のようにimport * as
で得たmod
に対してすぐプロパティアクセスする場合には、中身さえ同じならばconsole.log(mod.R)
でも変わりません。webpackはこれを理解して最適化を行うのです。
つまり、export名を変えられたり消されたりしてもコードの意味が変わらないようにすれば、import * as
構文を用いても最適化をしてもらうことができるのです。
具体的に言えば、import * as mod
のように得たモジュール名前空間オブジェクトは常にmod.foo
のようにプロパティアクセスの形で使えば大丈夫です。
エクスポート名は静的解析可能である
以上のような挙動の背景として、エクスポート名は静的解析可能であるという事実があります。JavaScriptのモジュールシステム(ES Modules)は、エクスポート名が静的可能であるように定義されています。つまり、あるモジュールが何という名前の変数(バインディング)をエクスポートするのかということは、モジュールを実際に実行しなくても、モジュールを構文解析するだけで決定可能なのです1。
これにより、import * as mod
のようにして得たmod
がどんなプロパティを持っているかということも、静的解析により決定可能になります。それゆえに、mod.foo
というプロパティアクセスの構文を使っている限り、mod.foo
がインポートされたモジュールのどの変数に対応するかも追跡可能です。エクスポート名のmanglingもこのことを理論的裏付けとして行われています。
なぜwebpackが最適化を行うのか
上記のような挙動は、webpackがこのような静的解析を実施したからこそ実現されています。では、なぜterserなどではなくwebpackがこのような最適化を行うのでしょうか。それは、webpackがES Modulesホストだからです。このことについては、以下の記事でも少し触れました。
つまり、import/exportをECMAScript仕様にしたがって解決するのはwebpackの役割なのです。だからこそ、webpackを通すとimport/exportはコードから消えてwebpackのランタイムに置き換えられます。これは、import/exportの解決という部分について、webpackは部分的にECMAScriptの実行環境として振舞っているということです。それゆえに、この部分に対してwebpackには好きなように最適化する権限が与えられます。
これにより、上述のような静的解析を根拠として、webpackはエクスポート名を改名(mangling)したり、余計なexportを消したり(tree shaking)することができます。
まとめ
以上のように、import * as
構文を使っても、場合によってはwebpackによる最適化の恩恵を受けることができます。筆者はよくio-tsをimport * as
構文と一緒に使います。
import * as t from 'io-ts'
const objType = t.type({
foo: t.string,
bar: t.string,
});
さすがにtype
やstring
といった変数名をインポートするとコードがややこしくなるのでこれは重宝します。
ちょうど上のコードのように、import * as
構文を使った際は必ずモジュール名前空間オブジェクトに対してプロパティアクセス構文を使うように気をつければ大丈夫です。
import * as
構文を使う際はうっかり最適化が無効になってしまわないように気をつけましょう。
以上です。この記事が良かったと思ったらぜひLGTMをお願いします。
……。
嘘です。
エンジニアリングにおいて「気をつける」というのはまともな解決策ではありません。ちゃんと仕組みで解決しましょう。
eslint-plugin-tree-shakable
今回、筆者はimport * as
構文を安全に使うためのESLint Pluginを製作しました。それがeslint-plugin-tree-shakable
です。
このルールを使うことで、tree shakingに悪影響を及ぼしてしまうような使い方をESLintで防ぎつつ、import * as
構文を活用することができます。
ぜひこちらのルールを使用してみてください。バグ報告や改善提案なども歓迎しています。
補足: メソッド呼び出しの場合
コメントで指摘をいただいたのですが、次のようなケースが多少問題です。
import * as mod from './mod';
mod.foo();
このようにメソッド呼び出しの構文でモジュール名前空間オブジェクトのプロパティを使用した場合、foo
が呼び出された際にその中のthis
がmod
になります。つまり、この構文の場合mod
自身が間接的に使用されており、tree shakingを阻害しそうです。
ただ、webpackで検証したところ、この場合でもtree shakingは有効のままでした。厳密に言えば最適化により挙動が変わってしまっていますが、そもそもモジュールからエクスポートされた関数がthis
に依存しているケースが稀なので許容されたのでしょうか。筆者としては、この挙動の方がio-ts
からエクスポートされたt.type
などをそのまま呼び出せるので助かります。
まとめ
ESLintを活用して楽しくimport *
ライフを送りましょう!
-
厳密に言えば、
export * from
構文の存在により、構文解析だけではなくモジュールグラフの解決も必要となります。 ↩