モダンブラウザに最適化した JavaScript を提供する

`typescript` や `babel` を用いて、 ES2015+ 記法で書かれたコードをレガシーブラウザでも動くようにコンパイルするのが現在一般的な手法となっています。

コンパイルにより生成される polyfill されたコードは、もちろんネイティブ実装の動作よりも遅くなります。

しかし、現在のモダンブラウザのほとんどは ES2015 をそのまま実行することが可能です。

基本的に [ES Module](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import) の機能が使えるケースは、 ES2015 並びに `async/await` がサポートされていると断定可能です。

この機能がサポートされているのは現在下記のブラウザです。

- Google Chrome 61+
- Edge 16+
- Firefox 60+
- Opera 48+
- Safari 10.1+

※参考資料: [MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#Browser_compatibility) 及び [caniuse.com](https://caniuse.com/#search=module)

一覧の通り、 Internet Explorer は残念ながら対象外です。

## コードのファイル分割手法

今までは、2つのアプローチでコードのファイル分割を行ってきました。

### 1. グローバル変数を経由する

```html:index.html
<script src="jquery.min.js"></script>
<script>
window.addEventListener('load', function () {
    $('#title').text('Hello world');
});
</script>
```

### 2. webpack などのバンドラを利用する

```html:index.html
<script defer src="vendor.js"></script>
<script defer src="index.js"></script>
```

```javascript:index.js
// webpack でバンドリングされた処理
```

---

しかし、 ES Module が利用出来るブラウザでは、下記のように書くことが可能です。

### 3. ES Module 形式を利用する

```html:index.html
<script module>
import { MyModule } from './my-module.mjs';

const myModule = new MyModule();
myModule.addHelloWorldTo(document.getElementById('title'));
</script>
```

```javascript:my-module.mjs
export class MyModule {
    addHelloWorldTo(element) {
        element.innterHTML = 'Hello world';
    }
}
```

このように、一つのモジュールスクリプトを加えるだけで、依存関係を `import` 構文で取得出来るようになります。

スクリプトは `.mjs` という拡張子でなければならないということに注意が必要です。

また、モジュール形式に対応していないブラウザでは `<script nomodule src="fallback.js"></script>` とすることで、フォールバックスクリプトを読み込ませることが可能です。

## ES Module を利用するメリット

わざわざ新しい概念である `ES Module` を利用するのは、他でもない **パフォーマンスの向上** に大きく寄与するためです。

[Deploying ES2015+ Code in Production Today — Philip Walton](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) こちらの記事ではこうあります。

- ES5(IE9+) に変換したコードが _175KB_ で読み込みに _367ms_ かかった
- ES2015+(ES Module) に変換したコードが _80KB_ で読み込みに _172ms_ かかった

**読み込み時間が約半分** になっています。ここで使われているコードは実用的なものではないかもしれませんが、明らかにパフォーマンスが良くなるという結果が出ています。

どういう理屈で速くなるのでしょうか。

### 1. 全てのスクリプトが遅延読み込みされる

`<script defer src="index.js"></script>` と書くことで、 DOM 描画をブロックすることなく非同期にスクリプトを読み込むことが出来ます。

ES Module では常にこれが有効になっていて、 `import` 文で必要に応じて非同期読み込みが行われるので不要なブロック時間がなくなります。

### 2. スクリプトファイルサイズが小さくなる

polyfill を減らすことが出来るので、各ファイルに記述が必要なスクリプトが減り、全体の読み込みサイズを抑えることが出来ます。

### 3. ネイティブ実装を利用できる

polyfill を使わずブラウザネイティブな実装を利用出来るので、その分だけ早くなります（バンドリングの設定によってはネイティブを優先するものもあるため、一概にこれで速くなるとは言い切れません）。

---

この通り、基本的にいいことだらけなので、今後のフロントエンド開発では ES Module による読み込みが増えてくるのではないかと思います。

IE や古いバージョンのブラウザでも、 `<script nomodule>` によるフォールバック機能を設ければ問題なく読み込み出来るので、レガシーブラウザでもバッチリです。

## 実装方法

実際に ES Module を利用して実装する方法はいくつかあります。

### 1. トランスパイルなしで書く

先ほどの例のように `<script type="module" src="index.mjs"></script>` を使えばそれだけで有効になります。

しかし、 `<script nomodule>` のフォールバックが必要な場合は用意することが大変なので、レガシーブラウザに対応する場合はこの手法はオススメ出来ません。

### 2. babel を利用する

[@babel/preser-env targets.esmodules](https://babeljs.io/docs/en/next/babel-preset-env.html#targetsesmodules) オプションを利用することで ON に出来ます。

### 3. webpack を利用する

`output.filename: 'main.mjs'` にしたコンフィグと `output.filename: 'main.es5.js'` にしたコンフィグを両方用意してあげれば、フォールバック処理込みで利用出来ます。

実際のサンプルコードは [こちら](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)

### 4. Vue CLI 3 を利用する

`vue-cli-service build --modern` オプションを追加することで可能です。

### 5. Nuxt.js を利用する

[API: The modern Property - Nuxt.js](https://nuxtjs.org/api/configuration-modern)

`modern` プロパティを nuxt.config.js 内で設定することで可能です。

`server` にすると nuxt.js サーバが User-Agent を見て自動的にスクリプトを配布してくれます。

`client` にすると、 `type="module"` と `nomodule` のスクリプトタグを両方提供します。 `nuxt generate` で静的サイトを作る場合に有用です。

### 6. tsc を利用する

TypeScript では `{"compilerOptions": {"module": "es2015"}}` を tsconfig.json で定義することで ES Module 形式で出力します。

## 利用時の注意

### Safari 10.1 は nomodule が効かないバグがある

[こちらの gist](https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc) で回避可能です。

### `Bare` import は使えない

```javascript
import { MyModule } from 'my-module.mjs';
```

のように、パスプレフィックスがない from 句はサポートされていません。同じパスであれば `./` プレフィックスをつける必要が有ります。

### 常に CORS で通信される

通常のスクリプトは CORS ではない読み込みを行いますが、 `type="module"` で読み込む場合は CORS を利用して読み込みます。サーバ側で `Access-Control-Allow-Origin: *` などのヘッダを追加する必要が有ります。

### クレデンシャルをつけない

通常同一オリジンに通信する場合はクレデンシャルを付けるのが `fetch` API などでのデフォルト動作ですが、現状いくつかのブラウザでは `crossorigin` 属性をつけない限りスクリプトのリクエスト時にクレデンシャルを渡さないようになっています。

### サーバの MIME types に気をつける

ES Module なスクリプトは拡張子を `.mjs` にする必要があります。デフォルトのサーバ設定では適切な MIME Types が指定されない場合があるので、正しく `text/javascript` と `mjs` を紐付けるようにしましょう。

### コンパイル時間が伸びる

モダンビルドを Vue や nuxt で行った場合は、モダンスクリプト(ES2015+)とレガシースクリプト(ES5)の両方をコンパイルする必要があります。単純に倍の時間がかかるようになりますので注意。

## 参考資料

* [API: The modern Property - Nuxt.js](https://nuxtjs.org/api/configuration-modern)
* [Browser Compatibility | Vue CLI 3](https://cli.vuejs.org/guide/browser-compatibility.html#modern-mode)
* [ECMAScript modules in browsers - JakeArchibald.com](https://jakearchibald.com/2017/es-modules-in-browsers/)
* [Deploying ES2015+ Code in Production Today — Philip Walton](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)
* [import | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import)
* [Can I use... Support tables for HTML5, CSS3, etc](https://caniuse.com/#search=module)