2021年11月15日
JavaScriptのバンドルとトランスパイルが不要なモダンWebアプリ
本記事は、原著者の許諾のもとに翻訳・掲載しております。
筆者はES6以前のVanilla JSがあまり好きではありませんでした。 そこで、バニラJavaScriptをなるべく書かなくていいように、2000年代を通じてさまざまなアプローチを追求してきました。最初はRJS(Ruby-to-JavaScript)、次はCoffeeScriptでした。どちらのアプローチも、バニラJavaScriptより楽しく書けるソースコードを、ブラウザが実行できるバージョンのJavaScriptへトランスパイルするものです。ある程度は、うまくいっていました。
とはいえ、これは明らかにその場しのぎの手段に過ぎず、ブラウザがより洗練されたJavaScriptを理解できる日を待ちわびていたのです。ただ、そんな日が来ることはなく、永久にその場しのぎでやり過ごすのかと思われる時期がしばらく続きました。
しかし、幸いなことにJavaScriptは改善を続け、2015年にはES6の仕様が確定し飛躍的な進歩を遂げました。そのかなり前から(そして確定後もしばらくの間)Babelトランスパイラによって未来のJavaScriptであるES6を書くことができました。
これは素晴らしいことでした。さまざまなブラウザがES6のサポートを開始する前から、大幅に改善されたJavaScriptでプログラムを書くことができたのです。まるでズルをしているような気分でした。何の対価も支払うことなく、得をしているように感じたのです。とはいえ、それは必ずしも正しくありませんでした。
Babelでトランスパイルが行われるようになったことで、複雑極まりないパイプラインやツールが増え続けることになりました。未来のJavaScriptは無償で書けるわけではなく、複雑性が増大し続けるという代償を伴っていたのです。目指すゴールがこれではないことは明らかでした。
とはいえ、webpackなどのツールによって、この変化に対応できたのはありがたいことです。webpackは複雑ですが、それを差し引いても価値があると思えるものでした。そこで筆者は、2016年にWebpackerを作成し、2017年にはRailsでJavaScriptを管理するためのアプローチとしてWebpackerを採用したRails 5.2をリリースしました。
それから5年が経ち、ついに開発現場の状況は変わりました。筆者はもはや、新しく登場したほとんどのアプリケーションにとって、複雑さに目をつぶってまでwebpackを使い続ける価値があるとは考えていません。ただし、webpackは完全に終わったわけではなく、依然として一部のアプリケーション(たとえばReactなど)で利用するのは理にかなっています。あくまでRailsにとっては優れたデフォルトツールではなくなったということです。
最初の決定的な変化は、すべての主要なブラウザでES6がサポートされるようになったことです。Chrome、Edge、Safari、FirefoxはいずれもES6を完全にサポートしています。最後に残ったのはIE11でしたが、Microsoftは今年、ありがたいことにサポート終了を宣言しました。
その結果、ES6をブラウザで実行するために、トランスパイルによって別のコードに変換する作業が必要なくなりました。ES6はそのままで問題なく実行され、変更は不要です。これは非常に大きな進歩です。
第二の決定的な変化はHTTP2が普及したことです。HTTP2では、1つの大規模なファイルを送信する代わりに、多くの小さなファイルをさほどのデメリットもなく送信できるようになりました。1回の接続で、必要なレスポンスをすべて多重送信できるのです。複数の接続を管理したり、複数のSSLハンドシェイクの費用を支払ったりする必要はありません。これは、すべてのJavaScriptを1つのファイルにバンドルしても、パフォーマンス上のメリットはあまりないことも意味します(ツリーシェイキングのメリットはまだありますが)。
実際、単独の大規模なバンドルの利用は、今や開発者の作業効率(バンドルにかかる時間が長いなど)以外にも複数のデメリットがあります。例えば、すべてのJavaScriptモジュールを1つのファイルにバンドルすると、1つのモジュールが変更されただけでバンドル全体が無効となってしまいます。この場合、ブラウザがバンドル全体を改めてダウンロードし、再びすべてをパースする必要があります。これはよろしくありません。
ちなみに、各モジュールを分ければ、1個のモジュールが無効となっても他のモジュールには影響しません。モジュールが20個あり、1個だけが変更される場合、他の19個はキャッシュされたままです。こうしたキャッシュの仕組みは、パフォーマンスの改善に熱心な人にとっては特にありがたいでしょう。
しかし、それ以上に強調しておきたいのは、バンドルする理由がパフォーマンスのためだけだったとしたら、今後バンドラーは一切不要になることです。シンプルに各モジュールを独立したファイルとして直接ブラウザに提供すれば済むのです。
どういうことか分かりますか?書きやすいJavaScriptを書くためのトランスパイルも、すべてのモジュールをパッケージ化するためのバンドルも必要ありません。つまり、ソースコードを別の何かに変換するためのJavaScriptツールチェーンは不要なのです。そもそも複雑性そのものが取り払われつつあるのです。
上記2つの決定的な変化に加え、パラダイムが変わる最後の一押しは、インポートマップです。インポートマップは、ES6(別名ESM)のモジュールについて、論理参照を可能にします。これまでの明示的なファイル参照方法の問題は、ダイジェスト化されたファイル名を利用した、キャッシュを長期間持続する標準的なアプローチとの相性が悪いことでした。
例えば、main-a6d26cef87d241eba5fa.js
のようなファイル名において、最後の英数字の部分はファイル全体のダイジェストを表しています。ダイジェストは各ファイルに固有で、ファイルが変更されるとダイジェストも変わります。同じファイルのダイジェストは決して変わらないため、ブラウザはファイルとダイジェストを永久にキャッシュすることができるわけです。もちろん、ファイルが変更されれば、ファイル名も新しくなります。このことは、キャッシュの有効期限をコントロールし、優れたパフォーマンスを実現する上でとても重要です。
しかし、もし50個のファイルが存在し、そのすべてについて"import { Controller } from './javascript/stimulus-a6d26cef87d241eba5fa.js'"
のようなimport文をプログラムの初めに書く必要があるとすればどうでしょうか。きっとうんざりするはずです。Stimulusに依存する箇所を変更するたびに、ファイルをひとつずつ更新しなければなりません。しかも、これらのファイルは個別に無効となるのです。
この問題を解決するのがインポートマップです。この機能はすでにChromeとEdgeに搭載されています。Firefoxも搭載を検討中のようです。Safariはインポートマップに全く言及していませんが、心配は無用です。完全な実行機能を持つshimを利用して、ESMをサポートするすべてのブラウザ(ES6をサポートするすべてのブラウザに該当)にインポートマップのサポートを導入できます。
インポートマップではインポートの対応関係を定義できます。つまり、"./javascript/stimulus-a6d26cef87d241eba5fa.js"
に代わって単に"stimulus"
と記載し、インポートマップは"stimulus": "/javascript/stimulus-a6d26cef87d241eba5fa.js"
とします。Stimulusを変更しても、参照文ではなくマップを変更するだけで済みます。
とはいえ、インポートマップを手作業でメンテナンスするのは少々手間がかかります。そこで、筆者はRails向けにimportmap-railsというgemを新たに作成しました。このgemでは、プログラムでマップを構築できます。shimも搭載されているため、すべてのブラウザで機能します。さらに、使い慣れた信頼性の高いアセットパイプラインエンジンのSprocketsを利用して、ダイジェスト作業を実施します。まさに完全なパッケージです。
トランスパイラとバンドラーが不要であるのに加え、インポートマップを生成できれば、Node.jsをローカル環境にインストールすることもなく、最新で素晴らしいWebアプリの開発環境を整えることができます。
Rails向けのHotwire gemは、StimulusとTurboの両方について、上記の設定に依存するように変更されています。このgemは、プログラムによるアクセスを利用してバックグラウンドでインポートマップの設定を行うため、application.jsにモジュールを直接インポートできます。
このように、膨大な量の複雑な作業をすっかりなくしてしまうことで、開発体験は言葉にできないほど劇的に変化します。まるで生まれ変わったようです。しかし、依然として未解決の課題や妥協点も残っています。
第一に、上記のとおり、JSXをコンパイルして利用するReactなど人気フレームワークの一部では、これまで紹介してきた技術は(今のところは?)いずれもうまくいきません。明示的なトランスパイルやコンパイルのステップが必要である場合、トランスパイラやコンパイラが求められるのは明らかです。これでは振り出しに戻ってしまいます。
第二に、Action Text、Active Storage、Action Cableなどの機能については、Rails上でJavaScriptを併用しつつ、HotwireをRubyの gemとアセットパイプラインで利用することはできますが、他にも改善が必要なJavaScriptエコシステムはまだ大量に残っています。
このエコシステムは、UMD(Node.jsが使用している旧来のパッケージングシステム)の代わりにESMパッケージを公開する必要があります。とはいえ、skypack.devなどのサービスは、UMDパッケージをESMに変換することで橋渡しとなるかもしれません。
Railsは、package.jsonファイルを使用し、npmを通じてパッケージを管理しない場合、これらのパッケージに依存し、更新を行う方法を見つけなければなりません。いくつかアイデアはあるものの、まだ具体化には至っていません。それまでの間は、単純にこれらのESMパッケージをダウンロードして、vendorディレクトリにローカルで保存すると良いでしょう。
このように、これまで紹介した技術はいずれも有望で、またES6、HTTP2、インポートマップの普及は飛躍的な進歩を実現しましたが、webpack(とWebpacker)を必要とするアプリが依然として数多く存在するのは明らかです。しかし、それは問題ではありません。私たちは無駄をなくすことで前進してきました。webpackやWebpackerはまだ完全にはなくなりませんが、不要になった人は大いに満足するでしょう。
この基本的な考え方に反する新たなエビデンスが出てこない限り、Rails 7.0のデフォルト設定はインポートマップに基づくものとし、Webpackerによるアプローチはオプションの選択肢にとどめる予定です。
複雑化したフロントエンドを修正し、シンプルな状態に回帰する動きは非常に遅れていました。しかし、ES6、HTTP2、インポートマップがまさにそれを実現してくれるでしょう。本当にうれしい限りです。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa