書いた JavaScript をそのまま動かすフロントエンド開発の未来のために必要なもの
2022年の sosukesuzuki 1人アドベントカレンダー 5 日目です。
大きめのテーマです。もしかしたら「うちでは書いた JS をそのまま配信してるぜ〜」って人もいるかもしれないでが。
最近の Web フロントエンド開発では、書いた JavaScript をそのまま動かさないことが多い
最近のフロントエンド開発ではエンジニアが書いた JavaScript をそのままブラウザで動かすことはほとんどないかもしれません。
例として最近流行のフレームワークを考えてみましょう。Next.js や Remix、Nuxt.js など、いずれも内部的にトランスパイラやモジュールバンドラを使い、エンジニアが書いた JavaScript を別の形へと変換してからユーザーのブラウザで動かすような仕組みになっています。
一昔前だと Next.js のようなフレームワークが今ほど発展していなかったこともあり、webpack や Babel を直接使っていたと思いますが、それも同じです。
どうしてそのまま動かすことができないのか
さて、なぜ我々は JavaScript をそのまま動かさず、なんらかの変換をしているのでしょうか。
筆者が考える JavaScript を変換する目的は、大きく分けて次の 3 つです。
1. ダウンレベルコンパイル
JavaScript には毎年のように新しい機能が追加され、進化しています。
(一部の)エンジニアは JavaScript の新しい便利機能を使ってコードを書きたいわけですが、ユーザーが使っているブラウザがその機能に対応しているとは限りません。そのため、新しい機能を使って書かれた JavaScript を十分古いバージョンの JavaScript に変換してから配布したほうが良い場合があります。
たとえば、次のような const
とアロー関数を使って書かれたコードを
const myFn = () => {
console.log("Hello, World!");
};
このような var
と関数式の形に変換する処理です。
var myFn = function myFn() {
console.log("Hello, World!");
};
この用途のための有名なツールとしては Babel、SWC などがあります。
2. 型の消去 / JSX の変換
TypeScript や Flow などの静的型付け AltJS (Alternative JavaScript) を使っている場合は、型注釈の部分を消去する必要があります。
たとえば、次のような型注釈の付いた関数宣言を
function myFn(name: string): void {
console.log(`Hello, ${name}!`);
}
このような型宣言のない形に変換する処理です。
function myFn(name) {
console.log(`Hello, ${name}!`);
}
また React 等を始めとした UI ライブラリは JSX の使用を前提としているので、JSX の変換も必要になります。
たとえば、次のような JSX での React コンポーネント定義を
const App = () => <h1>Hello, World</h1>;
このような関数呼び出しの形に変換する処理です。
import { jsx as _jsx } from "react/jsx-runtime";
const App = () =>
_jsx("h1", {
children: "Hello, World",
});
3. モジュールのバンドル
以前はブラウザの JavaScript にモジュールシステムがありませんでした。モジュールシステムなしで大規模なソフトウェアを開発するのは難しい場合があります。
そのため、エンジニアがモジュールシステムを使って書いたコードをブラウザで動作する形に変換する必要がありました。
たとえば、次のような複数のファイルからなるコードを
// index.js
import { a } from "./a.js";
console.log(a());
// a.js
export function a() {
return doSomething();
}
このような一つのファイルにまとめる処理です(実際にはもっと複雑で、出力されるファイルが複数であったりするとと思いますがここでは簡単な例を示します)。
// index.js
function a() {
return doSomething();
}
console.log(a());
実は現在では多くのブラウザが ECMAScript Modules を実行できます。そのためモジュールバンドラを使わなくてもモジュールシステムを使ったコードをブラウザで動かすことができます。
しかし ECMAScript Modules をそのまま使うと、モジュールを import するごとにフェッチされるため、アプリケーションの起動までにかかるオーバーヘッドが大きくなりがちです。なのでパフォーマンスのためのプラクティスとしてもモジュールバンドラが使われています。
この用途のための有名なツールとしては webpack、Rollup、esbuild などがあります。
どうしたら JavaScript をそのまま動かせるようになるのか
上で説明した 3 つのユースケースを解消できれば、少なくともそれらのために変換をする必要はなくなります。
1. ダウンレベルコンパイル
Internet Expolorer がその役目を終え Microsoft によるサポートが終了された今、以前と比べて多くのユーザーがいわゆるエバーグリーンブラウザを使うようになっていると思います。
それに伴ってダウンレベルコンパイルの必要性は低くなってきていると筆者は考えています。
サービスのユーザー層によっては、すでにダウンレベルコンパイルなしでも良いかもしれません。
現実的には、新しすぎる言語機能を使うとユーザーの環境で動かない可能性が高いので、ESLint 等で制約をかける必要はあると思いますが、それでも十分便利な JavaScript をダウンレベルコンパイルなしで書けるのではないかと思います。
2. 型の消去 / JSX の変換
これは難しい問題です。
型のストリップについては、現在 Stage 1 の Type Annotations プロポーザルが ECMAScript に入れば不要になります。しかし現実的にはこのプロポーザルのステージを進めていくのはかなり困難だと思っています。
JSX の変換については、Proposal: ESX as core JS feature として現在議論されているようです(まだ TC39 プロセスにおけるプロポーザルではないようです)。しかし、個人的にはこちらもなかなか難しいのではないかと思っています。
とはいえ、これらのプロポーザルが ECMAScript に追加されれば、型や JSX のために変換を行う必要はなくなるでしょう。
3. モジュールのバンドル
我々の開発からモジュールバンドラを取り除くにあたってのボトルネックは、前述のとおりモジュールを import するごとにフェッチが起こることによるオーバーヘッドです。
これを解消するための技術として、筆者としては次の 2 つに注目しています。
これらの技術はいずれも、複数のファイルをまとめて 1 つのファイルとしてフェッチできるようにします(この表現は厳密には違いそうですが)。
長くなるし、技術的詳細の理解に自信がないのでこれらの技術についてここで詳しく解説することはしません。それぞれの Explainer や、日本語での解説記事を参照してください。
Subresource loading with Web Bundles は Jxck さんの Webbundle によるサブリソース取得の最適化 | blog.jxck.io が詳しいです。
Bundle Preloading (旧 Resouce Bundles) は ゆき さんの Web ページのサブリソースを一つにまとめる Resource bundles (Bundle preloading) とは - ASnoKaze blog が詳しいです。
これらの技術があれば ECMAScript Modules を使って書いたコードをそのまま配信しつつ、ランタイムでのオーバーヘッドを減らせるでしょう。
そもそも、なぜそのまま動かす必要があるのか
サステナブルでない複雑な変換レイヤの存在
webpack や Babel をはじめとした JavaScript のトランスパイラやモジュールバンドラというのは複雑です。多くのバグがありますし(筆者もトランスパイラやモジュールバンドラのバグをいくつも見つけたことがあります)、その開発コストも膨大です。
さらにその開発は、OSS や一部の企業(Vercel とか)によって提供・維持されておりサステナブルとは言えません。
ここ数年のエコシステムの発展によって明らかになった Web 開発におけるユースケースを、ツール群から Web そのものに委譲していく必要があるのではないかと、筆者は考えています。
もったいない
現実の問題もありますが、あくまで筆者の感想として、もったいないと思っています。
たとえば ECMAScript Modules の振る舞いは ECMAScript と HTML で標準化されており、多くのブラウザですでに実装されていて、ちゃんと動きます。
それにも関わらず、(ECMAScript Modules に限らず)その機能をプロダクションで使えていないのは Web のポテンシャルを活かしきれていなくてもったいないような気がしてしまいます。
おまけ
Minify は?
書き終わってから minify のことを思い出したのでおまけとして書き足しています。
少なくとも筆者は minify の役割に代わる Web 技術を知りません(なにかあるんですかね)。
なので、当然ではありますが minify を導入するコストと導入した結果得られるパフォーマンスとのトレードオフで決めなければいけません。
たとえば、サービス提供対象エリアの通信路が速く、静的な JavaScript ファイルに対しては CDN でキャッシュするなどして十分なパフォーマンスが出るということであれば minify は不要でしょうし、そうでなければ使われると思います。
参考リンク
- Frameworks
- Transpilers
- Module bundlers
- Static type checkers
- TC39
- WICG