import * as 構文とパフォーマンス最適化


JavaScriptには、`import * as` という構文があります。これは、インポート先のモジュールの中身全部をオブジェクト（モジュール名前空間オブジェクト）として取得できる構文です。

```js
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を使用しています。

```js:webpack.config.js
module.exports = {
  mode: "none",
  optimization: {
    providedExports: true,
    usedExports: true,
    mangleExports: "deterministic",
  },
};
```

## 最小構成で検証してみる

では、`import * as`構文に対してwebpackがどう振る舞うのか最小構成で検証してみましょう。

```src/mod.js
export const foo = "foo";
export const bar = "bar";
```

```src/index.js
import * as mod from "./mod";

console.log(mod.foo);
```

出力結果全体は長いので畳んでおきます。

<details>
  <summary>dist/main.js</summary>

```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);

})();

/******/ })()
;
```
</details>

### webpackの出力結果

`src/mod.js`に相当する出力は次のようになっています。

```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`に相当する出力は次のようになっています。

```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`構文を使うと最適化が効かない場合というのも存在します。次の場合がそうです。

```js:src/mod.js
export const foo = "foo";
export const bar = "bar";
```

```diff_javascript:src/index.js
 import * as mod from "./mod";

- console.log(mod.foo);
+ console.log(mod);
```

### webpackの出力結果

上の入力に対しては、webpackは次のような結果を出力します。

```js:src/mod.jsに相当する部分
/* 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";
```

```js:src/index.jsに相当する部分
/* 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）は、エクスポート名が静的可能であるように定義されています。つまり、あるモジュールが何という名前の変数（バインディング）をエクスポートするのかということは、モジュールを実際に実行しなくても、モジュールを構文解析するだけで決定可能なのです[^note_export_star]。

[^note_export_star]: 厳密に言えば、`export * from`構文の存在により、構文解析だけではなくモジュールグラフの解決も必要となります。

これにより、`import * as mod`のようにして得た`mod`がどんなプロパティを持っているかということも、静的解析により決定可能になります。それゆえに、`mod.foo`というプロパティアクセスの構文を使っている限り、`mod.foo`がインポートされたモジュールのどの変数に対応するかも追跡可能です。エクスポート名のmanglingもこのことを理論的裏付けとして行われています。

## なぜwebpackが最適化を行うのか

上記のような挙動は、webpackがこのような静的解析を実施したからこそ実現されています。では、なぜterserなどではなくwebpackがこのような最適化を行うのでしょうか。それは、**webpackがES Modulesホストだから**です。このことについては、以下の記事でも少し触れました。

https://qiita.com/uhyo/items/22d851c3cbd2570864ce

つまり、import/exportをECMAScript仕様にしたがって解決するのはwebpackの役割なのです。だからこそ、webpackを通すとimport/exportはコードから消えてwebpackのランタイムに置き換えられます。これは、import/exportの解決という部分について、webpackは部分的にECMAScriptの実行環境として振舞っているということです。それゆえに、この部分に対してwebpackには好きなように最適化する権限が与えられます。

これにより、上述のような静的解析を根拠として、webpackはエクスポート名を改名（mangling）したり、余計なexportを消したり（tree shaking）することができます。

## まとめ

以上のように、`import * as`構文を使っても、場合によってはwebpackによる最適化の恩恵を受けることができます。筆者はよく[io-ts](https://github.com/gcanti/io-ts)を`import * as`構文と一緒に使います。

```ts
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`です。

https://github.com/uhyo/eslint-plugin-tree-shakable

このルールを使うことで、tree shakingに悪影響を及ぼしてしまうような使い方をESLintで防ぎつつ、`import * as`構文を活用することができます。

![eslint-plugin-tree-shakableが動作している様子。 mod.foo に対しては警告しないが mod 単体の使用に対しては警告される。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/0ae70ade-56ec-1abf-14bd-cf4bd14e98b1.png)

ぜひこちらのルールを使用してみてください。バグ報告や改善提案なども歓迎しています。

## 補足: メソッド呼び出しの場合

コメントで指摘をいただいたのですが、次のようなケースが多少問題です。

```js
import * as mod from './mod';

mod.foo();
```

このようにメソッド呼び出しの構文でモジュール名前空間オブジェクトのプロパティを使用した場合、`foo`が呼び出された際にその中の`this`が`mod`になります。つまり、この構文の場合`mod`自身が間接的に使用されており、tree shakingを阻害しそうです。

ただ、webpackで検証したところ、**この場合でもtree shakingは有効のままでした**。厳密に言えば最適化により挙動が変わってしまっていますが、そもそもモジュールからエクスポートされた関数が`this`に依存しているケースが稀なので許容されたのでしょうか。筆者としては、この挙動の方が`io-ts`からエクスポートされた`t.type`などをそのまま呼び出せるので助かります。

## まとめ

ESLintを活用して楽しく`import *`ライフを送りましょう！