19. 余談: JavaScript を真剣に受け止める
TC39 のメンバーは 1990 年代の後半から JavaScript を職業的プログラマのための真剣な言語として再設計する試みを始めた。ブラウザおよび関連するプラットフォームの開発者も 2000 年代の後半には JavaScript が彼らのプラットフォームの一部であり、真剣なエンジニアリングが必要なことを認識した。
19.1 JavaScript の性能革命
1995 年 5 月に Brendan Eich が Mocha を作ったとき、性能は懸念でもなければ目標でもなかった。JavaScript で書かれたプログラムがこの世に存在しなかった上に、書かれると予想されたプログラムは効率に優れる他の言語で書かれたオブジェクトを簡単に操作するものだったからである。多少でも複雑なアルゴリズムを JavaScript でコーディングすることは想定されていなかった。初期の JavaScript エンジンは単純なバイトコードインタープリタあるいはパースツリー評価器を使って JavaScript を直接評価し、単純なメモリ管理手法を使っていた。Lisp, Smalltalk, Self といった動的言語向けに 1980 年代から 1990 年代の初頭にかけて開発された手の込んだ高性能実装テクニックは初期の JavaScript エンジンで一切使われていなかった。Netscape/Mozilla の SpiderMonkey と Microsoft の JScript エンジンの基本的なアーキテクチャは 10 年にわたってほぼそのままだった。その間に新しい ES3 レベルの言語機能が追加されセキュリティの問題は解決したものの、この時期の性能向上はムーアの法則 [Moore 1975] に従ってハードウェア性能が向上した結果だと言える。このころブラウザの JavaScript エンジンのメンテナンスはたいてい一人のソフトウェア開発者が片手間に担当する仕事だった。
2000 年代の前半に AJAX スタイルを採用する JavaScript ベースのウェブアプリケーションが流行し、そういった第一世代のエンジンの低い性能が真剣な問題になり始めた。2006–2007 年にはウェブ開発者が性能の問題に関して声を上げるようになり、ブラウザベンダーは JavaScript エンジンの性能を改善するための専門チームを編成し始めた。性能改善に向けた重要な最初の一歩は性能を計測できるようにすることであり、Apple の WebKit語 チームは SunSpider [Stachowiak 2007a] と呼ばれる JavaScript ベンチマークスイートを性能計測のために作成した。SunSpider は完璧から程遠くテストケースも比較的小さかったものの、テストケースは実際のウェブアプリケーションのコードから取られていた。SunSpider がリリースされるとウェブアプリケーション開発者コミュニティはすぐにブラウザの JavaScript 性能を比較し、その結果について話すようになった。ブラウザゲーム理論によりブラウザベンダーは一般に JavaScript の基礎的機能で競争できなかったものの、JavaScript の性能に関する競争は SunSpider によって始まった。
異なるベンダーは異なる道筋で高性能な JavaScript エンジンを達成しようとした。2006 年、Google は後に Chrome語 ブラウザとなるプログラムの開発を開始する。Chrome の JavaScript エンジン V8語 の開発を率いたのは Lars Bak であり、彼は Smalltalk, Self, Java の仮想マシンを通して学んだテクニックを使って V8 を作成した [Google 2008b]。2008 年 9 月に Chrome がリリースされると、Chrome は JavaScript の優れた性能の新たなベースラインとなった。このころ行われた実験 [Hobbs 2008] では、Google のベンチマーク [Google 2008a] において V8 が当時最新の Firefox に搭載された SpiderMonkey より 約 10 倍高速だったと報告されている1。ただし SunSpider ベンチマークを使うと V8 は約 2 倍速いだけだった。
性能改善に向けた Mozilla の最初のアプローチは TraceMonkey [Gal et al. 2009] と呼ばれ、Andreas Gal がカリフォルニア大学アーバイン校で行った卒業研究をベースとして開発した。既存の SpiderMonkey インタープリタをトレース駆動のコード特殊化 JIT コンパイラで拡張したもので、動的に特定したホットスポットに対して最適化されたネイティブコードを生成する。Apple の SquirrelFish Extreme [Stachowiak 2008a] (別名 Nitro) は Self および高性能 Lua の実装を参考にしていた。Microsoft は最初レガシーの JScript エンジンを IE8 に向けてインクリメンタルに再設計することを計画していたものの、IE9 で完全に新しい JIT ベースの JavaScript エンジンを作成した。
19.2 CommonJS と Node.js
誕生したそのときから、JavaScript は基礎的なスクリプト機能を提供するためにサーバープラットフォームでもホストされてきた。しかし各プラットフォームはユニークであり、提供される JavaScript API はそれぞれ異なっていた。ブラウザ外で動作する JavaScript アプリケーション用の相互運用が行えるドメイン非依存な共通環境は JavaScript が生まれてから 15 年にわたって存在しなかった。2009 年 1 月、Adobe と Mozilla で働いた経験があり当時は Khan Academy で働いていた Kevin Dangoor は今がこの状況を変えるときだと決心した。彼はブログ記事 [Dangoor 2009] で問題を説明し、オンラインの議論グループと wiki を通してこの問題を解決することをサーバーサイド JavaScript コミュニティに呼びかけた。一年後に書かれたフォローアップのブログ記事 [Dangoor 2010] で、彼は最初に作ろうとしていたものを次のように説明した:
- モジュールシステム
- どのインタープリタでも使える標準ライブラリ
- いくつかの標準インターフェース
- パッケージシステム
- パッケージレポジトリ
最初の一週間で 224 人のメンバーが議論グループに参加し [Kowal 2009a]、その多くがプロジェクトへの貢献に興味を示した。この取り組みは最初 ServerJS と呼ばれていたものの、2009 年 8 月に CommonJS語 へと改名された。この取り組みがもたらす技術はサーバーを超えた応用性を持つと考えられたためである。ServerJS/CommonJS は実装よりも仕様を書くことに集中した。
2009 年の 4 月には最初のモジュール仕様 [CommonJS Project 2009] が完成した。この CommonJS モジュール仕様は Kris Kowal と Ihab Awad による設計をベースとしていた [2009a]。
CommonJS モジュールは JavaScript 関数の本体であり、そのスコープには他のモジュールと対話するための束縛がいくつか含まれる。モジュールは非同期なモジュールローダーによって実装され、モジュールローダーはモジュールのソースコードをフェッチし、そのコードをスケルトンの関数定義に包み、そうして合成された関数を呼び出してモジュール自身およびモジュールとその他のモジュールとの関係を初期化する。図 28 に示すように、モジュールスコープの宣言は合成された関数のローカル変数になる。またシステムの制御フックは合成された関数のパラメータとして公開され、このパラメータの値はローダーによって提供される。合成された関数の require
パラメータは関数であり、リクエストされたモジュールの読み込み処理を非同期に行って読み込まれたモジュールの exports
値を返す2。デフォルトの exports
の値はローダーが渡すオブジェクトである。モジュールのコードが値をエクスポートするときは、exports
オブジェクトにプロパティを作成する。これは実行時に起こる動的な処理であり、モジュールの名前、実際の exports
の値、そして exports
のプロパティ名はどれも動的に生成される。この仕組みにより、アプリケーションが必要とするモジュールやモジュール間で共有される要素を事前に特定するのが困難あるいは不可能になる。
CommonJS モジュール
// moda.js - ソース
var modp = require("modp");
exports.n = modp.p++;
exports.modName = "prefix" + exports.n;
// modb.js - ソース
var modx = require(require("moda").modName);
var propName = Object.keys(modx)[0];
exports[propName] = modx[propName];
変換後のコード
// moda.js - CJS 展開後
(function(exports, require, module) {
var modp = require("modp");
exports.n = modp.p++;
exports.modName = "prefix" + exports.n;
});
// modb.js - CJS 展開後
(function(exports, require, module) {
var modx = require(require("moda").modName);
var propName = Object.keys(modx)[0];
exports[propName] = modx[propName];
});
CommonJS モジュールをいち早く採用したプロジェクトの一つに Ryan Dahl によって 2009 年に開発が始まった Node.js語 がある。Node.js は JavaScript ベースのオープンソースプラットフォームであり、大量のクライアント接続を並列に処理できるサーバーアプリケーションを構築するために作成された。Node.js は広く普及した非同期 I/O モデルを公開するライブラリを持った JavaScript プログラミング環境を提供し、JavaScript のコールバックと単純化されたブラウザのイベントループを共通の POSIX インターフェースに統合する。その実装は Google の JavaScript エンジン V8 をスタンドアローンに使えるようラップしたもの、CommonJS のモジュールローダー、そして POSIX API をはじめとした高レベルなファイル/ネットワーク操作をノンブロッキングに C で実装したモジュールから構成される。最初のパブリックなバージョンは 2009 年 5 月にリリースされた [Node Project 2009] ものの、2009 年 11 月に Dahl [2009] が jsconf.eu でプレゼンテーションを行うまで大きな注目は集まらなかった。その後すぐに Dahl は Joyent に雇われ、Node.js の開発は 2015 年に Node Foundation に移る [Node Foundation 2018] まで Joyent の管理とサポートを受けた。
当初 Node.js はサーバーアプリケーションを構築するための技術として認識されていたものの、最終的に Node.js は軽量な組み込みデバイスを含む様々なプラットフォーム向けの汎用プログラミング言語として JavaScript を実行可能にするプラットフォームとなった。高性能な V8 エンジンと組み合わさった Node.js の I/O モジュールは Python や Ruby といった動的アプリケーション言語と同程度の機能を持ち、多くの場合で上回る速度を持っていた。Node.js は JavaScript でコマンドラインアプリケーションを書くときのデファクトスタンダードな方法となった。Node.js のおかげで、JavaScript を習得したウェブプログラマは自身のスキルを他の種類のアプリケーションやブラウザ以外の環境でも活かせるようになった。元々クライアントウェブアプリケーションの開発者は他に選択肢がないために JavaScript でプログラムを書いていたのに対して、Node.js 開発者の多くは JavaScript でプログラムしたいがために Node.js を選択した。
19.3 JavaScript: ブラウザユニバーサルランタイム
JavaScript はブラウザプラットフォーム間の相互運用性を規定する規格の集合の一部となっている言語である。全てのブラウザで利用できるとウェブ開発者が期待できる唯一の言語でもある3。Java, Adobe Flash, Microsoft Silverlight語 といった言語環境はこの標準プラットフォームに含まれておらず、ブラウザ特有のアドオン機構を使って ── 言語環境がそのブラウザをサポートする場合にのみ ── ブラウザに組み込まれていた。通常そういった言語エンジンはブラウザユーザーが個別にインストールしなければならず、DOM ベースのグラフィックスモデルといったブラウザ標準のサービスに完全に溶け込むとは限らなかった。
ブラウザゲーム理論によると、他のプログラミング言語で標準ブラウザプラットフォームを拡張する試みが成功する確率は非常に低い。ウェブ開発者に広く採用される保証のない状態でウェブ向けの新しい言語を設計、実装、宣伝するのはブラウザベンダーにとって大変な投資が必要となる。採用を伸ばすには全ての主要なブラウザを説得して、競争相手が設計したユーザーベースが小規模あるいは存在しない言語を、将来のメンテナンスの重荷になることを承知でサポートさせなければならない。例えば Google は 2011 年に Dart語 言語を開発し、ウェブのためのより良い言語だと宣伝した [Krill 2011]。Google は Dart 仮想マシンを搭載した実験バージョンの Chromium語 (オープンソースで公開される Chrome ブラウザの基礎) の配布も行ったものの、Chrome の製品バージョンに Dart が取り入れられることはなく、他のブラウザも採用しなかった。
AJAX (Web 2.0) スタイルのアプリケーションが 2005 年に誕生し成長すると、ウェブ開発者は大規模で複雑なウェブアプリケーションを書き始め、一部の開発者はそういったアプリケーションへの適性が ES3 レベルの JavaScript より高いプログラミング言語を模索し始めた。ウェブページの一部として任意のブラウザで実行されるコードを書く必要があって、それを JavaScript 以外の言語で書きたい (あるいは書く必要がある) とき、開発者はどうするべきだろうか? 唯一の選択肢は、どうにかして他の言語のランライムサポートを JavaScript から提供することである。しかし 2000 年代の中頃の JavaScript エンジンはいまだに比較的低速なインタープリタとして実装されており、さらに JavaScript は効率的なインタープリタを書きやすい言語ではなかった。二重に遅いインタープリタは魅力的な解決法とは言えない。これより望みのある方法は、ソースからソースへの変換を通して他の言語を JavaScript でホストする ── 他の言語で書かれたソースコードを JavaScript コードに変換するコンパイラを作り、そのコンパイラの出力をブラウザの JavaScript エンジンでネイティブに実行するというものである。コンパイル元の言語の意味論と JavaScript の意味論的にそれなりに似ていれば、この方法でコンパイルされたプログラムの実行時性能は手書きの JavaScript のものに比較的近くなる。
2006 年 5 月に一般に公開された Google Web Toolkit (GWT) [Google 2006] は広く使われる AJAX ツールキットとして初めてソースからソースへの変換を利用した。GWT は Java から JavaScript へのコンパイラを持つ。GWT は Google においてユーザーと対話するいくつかの重要なウェブアプリケーションで問題なく使われ、さらに Google 外でも広く使われた。GWT の成功は JavaScript をターゲットとしたソースからソースへの変換の実現可能性を証明し、他の様々な言語からのコンパイラがこれに続いた。2011 年 1 月の時点で JS にコンパイルできる言語のリスト [Ashkenas et al. 2011] には 19 の言語が並んでいる。このリストの 2018 年バージョン [Ashkenas et al. 2018] には JavaScript に変換できる言語あるいは JavaScript がホストできる言語が 270 個以上示されている。オモチャの言語や不完全な実装も含まれてはいるものの、たくさんのユーザーを持つ真剣なコンパイラも多くある。JavaScript をターゲットとした Dart コンパイラさえ存在する。
ソースからソースへの変換はウェブページにおけるレガシー言語のサポートに使われただけではなく、新しい言語や JavaScript の言語拡張を実験する手段も提供した。最も成功したソースからソースへのコンパイラの一つが 2009 年から 2010 年にかけて Jeremy Ashkenas によって開発された CoffeeScript語 [Ashkenas 2010] である。Ashkenas はウェブ開発者になる前に Ruby 言語でプログラミングをした経験があり、Ruby の比較的記号を使わない構文と Python スタイルの意味を持った空白を JavaScript が使う C スタイルの構文より気に入っていた。彼は JavaScript を構文的に新しくしたものとして CoffeeScript を作成しつつも、内部では JavaScript の実行時意味論をそのままにした。Ashkenas [2009] は CoffeeScript を発表したとき次のように説明している:
JavaScript の豪華なオブジェクトモデルは Java 風の構文の中にずっと隠れてきた。
CoffeeScript は文よりも式を優先し、記号によるノイズを減らし、素敵な関数リテラルを持たせることでこの JavaScript の良い部分を露出させようとする試みである。例えば CoffeeScript コード
square: x => x * x
は、次の JavaScript コードに変換される:
var square = function(x) { return x * x; };
「素敵な関数」の他にも、CoffeeScript はクラス宣言や分割代入といったプログラミングを便利にする構文であって簡単に JavaScript コードへ変形できるものを持っていた。CoffeeScript の機能には ECMAScript Harmony で検討されていた機能に似ているものが多くある。CoffeeScript は JavaScript プログラマがそういった機能に関心を持っていることを証明した。CoffeeScript はたちまち非常に有名になり、多くの一流ウェブサイト開発者に採用された。ES2015 が広く利用可能になってからは CoffeeScript の利用は衰えていった。
2011 年 5 月の JSConf で Brendan Eich は Jeremy Ashkenas と共に壇上に上がり、CoffeeScript について、そして Harmony における JavaScript の進化で CoffeeScript が果たした役割について語った。このプレゼンテーションで Eich [2011c] は CoffeeScript のようなソースからソースへの変換を行うコンパイラを指す単語「トランスパイラ語」を「今日の単語」として紹介した。この意味で「トランスパイラ」という単語が使われたのはこれが初めてではなかったものの、Eich の講演の前には広く知られておらず、使われてもいなかった。この講演の後「トランスパイラ」は JavaScript の開発者コミュニティおよびそれ以外の場所で広く使われるようになった。
Alon Zakai [Zakai 2011] が開発した Emscripten は C/C++ コードを高速な JavaScript コードに変換するトランスパイラである。JavaScript の 32 ビット算術を使ったコーディングパターン (§3.7.3) とバイナリのデータ構造 TypedArray
を使えば JIT ベースの JavaScript エンジンが簡単に最適化できる C 実行環境を定義できるという観察に基づいて Emscripten は作られている。Emscripten は asm.js [Herman et al. 2014] の着想の元となった。asm.js は Emscripten のようなコンパイラが生成すべき、そして JavaScript エンジンが認識したうえで最適化すべき JavaScript コードパターンを定める仕様である。asm.js の成功は WebAssembly の開発 [Haas et al. 2017] に繋がった。WebAssembly は C/C++ などの低レベル言語をコンパイルするときのターゲットとして利用できるバイトコードレベルのインターフェースで JS エンジンを拡張する。
-
本稿の著者の一人が 2018 年に当時最新バージョンの V8 を使って 2011 年製の古い iMac 上で同じベンチマークをブラウザ内で実行したところ、報告された結果は Hobbs が 2008 年に得た V8 の結果より約 20 倍高速だった。 ↩︎
-
特定のモジュールに対する最初の
require
だけが完全な読み込み処理を行う。同じモジュールに対する二回目以降のrequire
リクエストでは最初のrequire
でローダーが返したexports
の値がすぐに返される。 ↩︎ -
そうは言っても、ウェブページ開発者はブラウザのユーザーが JavaScript を無効化している可能性や JavaScript サポートを含まないプログラムがページを処理する可能性を考慮すべきである。 ↩︎