219
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-28

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

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

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

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側でfooRにリネームされたことに対応して、それを使う側もRになっています。

このことから分かることは、import * as構文を使ってもtree shakingが効くし、export名のmanglingも行われるということです。manglingというのは、(JavaScriptの文脈では)変数名などを短く書き直してコードサイズを減らすことを意味します。今回の場合fooRになっています。

ちなみに、import { foo } from "./mod"のようにnamed importを行なった場合もfooRにしてもらえます。import * as構文もnamed importと同等のサポートを受けられるということですね。

なお、const fooというように変数名がmanglingされていなかったりconst barが残っているのが気になるかもしれませんが、これは問題ありません。なぜなら、このあたりのmanglingや消去を担当するのはwebpackではなくminifier (terserなど)の役目だからです。

最適化が効かない場合

実のところ、import * as構文を使うと最適化が効かない場合というのも存在します。次の場合がそうです。

src/mod.js
export const foo = "foo";
export const bar = "bar";
src/index.js
 import * as mod from "./mod";

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

webpackの出力結果

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

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";
src/index.jsに相当する部分
/* harmony import */ var _mod__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


console.log(_mod__WEBPACK_IMPORTED_MODULE_0__);

見て分かるように、fooRに変わるというようなmanglingが行われていません。また、foobarも消されていません。

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-tsimport * as構文と一緒に使います。

import * as t from 'io-ts'

const objType = t.type({
  foo: t.string,
  bar: t.string,
});

さすがにtypestringといった変数名をインポートするとコードがややこしくなるのでこれは重宝します。

ちょうど上のコードのように、import * as構文を使った際は必ずモジュール名前空間オブジェクトに対してプロパティアクセス構文を使うように気をつければ大丈夫です。

import * as構文を使う際はうっかり最適化が無効になってしまわないように気をつけましょう。

以上です。この記事が良かったと思ったらぜひLGTMをお願いします。

……。

嘘です。

エンジニアリングにおいて「気をつける」というのはまともな解決策ではありません。ちゃんと仕組みで解決しましょう。

eslint-plugin-tree-shakable

今回、筆者はimport * as構文を安全に使うためのESLint Pluginを製作しました。それがeslint-plugin-tree-shakableです。

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

eslint-plugin-tree-shakableが動作している様子。 mod.foo に対しては警告しないが mod 単体の使用に対しては警告される。

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

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

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

import * as mod from './mod';

mod.foo();

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

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

まとめ

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

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

219
110
2

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
219
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?