Posted at

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

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

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

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

基本的に ES Module の機能が使えるケースは、 ES2015 並びに async/await がサポートされていると断定可能です。

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


  • Google Chrome 61+

  • Edge 16+

  • Firefox 60+

  • Opera 48+

  • Safari 10.1+

※参考資料: MDN 及び caniuse.com

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


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

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


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


index.html

<script src="jquery.min.js"></script>

<script>
window.addEventListener('load', function () {
$('#title').text('Hello world');
});
</script>


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


index.html

<script defer src="vendor.js"></script>

<script defer src="index.js"></script>


index.js

// webpack でバンドリングされた処理



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


3. ES Module 形式を利用する


index.html

<script module>

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

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



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 こちらの記事ではこうあります。


  • 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 オプションを利用することで ON に出来ます。


3. webpack を利用する

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

実際のサンプルコードは こちら


4. Vue CLI 3 を利用する

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


5. Nuxt.js を利用する

API: The modern Property - Nuxt.js

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 で回避可能です。


Bare import は使えない

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/javascriptmjs を紐付けるようにしましょう。


コンパイル時間が伸びる

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


参考資料