JavaScriptをClosure CompilerのADVANCEDモードに完全対応させるその方法!

こんにちは。会社を抜けだしてiPhone5を予約しに行ったら熱中症になりかけたkintone開発チームの天野(@ama_ch)です。

kintoneでは10万行以上のJavaScriptが動いており、ライブラリにはClosure Libraryを利用しています。この規模になると、1ページあたりで読み込むjsファイルの容量が数MB単位(!)になってしまいます。そこで、通信量を減らすためにClosure CompilerでJavaScriptをミニファイしています。今となっては特に珍しい話ではないですね。

ところで、Closure Compilerの最適化レベルには3段階あるのをご存知でしょうか。

・WHITESPACE_ONLY コメントとホワイトスペースの除去など ・SIMPLE_OPTIMIZATIONS(デフォルトレベル) WHITESPACE_ONLY に加え、ローカル変数のリネームや関数内の引数名の短縮化など ・ADVANCED_OPTIMIZATIONS SIMPLE_OPTIMIZATIONS に加え、アグレッシブなリネーム、関数のインライン展開、変数展開、オブジェクトのプロパティの平坦化、到達不能コードの削除などなど

実際にkintone内のあるページで読み込まれるjsファイルを、各レベルで最適化した時のサイズは以下のようになりました。

圧縮後(raw) 圧縮後(gzip)
最適化前(546ファイル) 4,801,391 バイト 965,960 バイト
WHITESPACE_ONLY 2,855,608 バイト 521,857 バイト
SIMPLE_OPTIMIZATIONS 1,830,823 バイト 386,843 バイト
ADVANCED_OPTIMIZATIONS 674,615 バイト 224,404 バイト

ADVANCED_OPTIMIZATIONSで最適化すると、gzip後のサイズでSIMPLE_OPTIMIZATIONの6割以下にまで圧縮されています。かなり効果があることがわかりますね!

JavaScript圧縮ツールはClosure Compiler以外にも沢山ありますが、普通はSIMPLE_OPTIMIZATIONS相当の処理をします。Closure CompilerのADVANCED_OPTIMIZATIONSは相当高い圧縮率が実現できますが、圧縮前のコードとの互換性が失われてしまうため、実際に使うとなると「ADVANCEDコンパイルしても動くコード」を書かなければいけません。kintoneではすべてのページがADVANCEDコンパイルに対応しており、高い最適化の恩恵を受けています。

さらっと「すべてのページがADVANCEDコンパイルに対応」と言いましたが、かなりの苦労を乗り越えてやっと対応してきた経緯があります。

前置きが長くなっちゃってすいません。それでは、これからkintoneをADVANCEDコンパイルに対応するために実践したテクニックを紹介します。

ドキュメント

ADVANCEDコンパイルするための基本的な情報はこちらにまとまっています。 Understanding the Restrictions Imposed by the Closure Compiler Advanced Compilation and Externs

一貫した方法でプロパティにアクセスする

Closure Compilerには、 文字列リテラルは決して置き換えられない というルールがあります。このルールにより、 obj.hoge と obj['hoge'] はADVANCEDコンパイル後に違うものを指すようになります。

コンパイル前

コンパイル後

objは変数名もプロパティ名もリネームされていますが、hasOwnPropertyとobj['onamae']でアクセスしているところは文字列リテラルが使われているためリネームされません。結果、a.onamaeという存在しないプロパティを参照するようになってしまいました。これが正しく動くように修正したものが、次の2つのコードです。

コンパイル前

コンパイル後

どちらも一貫した方法でプロパティにアクセスするよう修正したものです。前者は文字列リテラルを使わない方法で、後者は文字列リテラルを使う方法です。前者の方がリネームが効く分コンパイル後のコードサイズが小さくなるので、好ましい方法と言えます。一方で、サーバから受け取ったJSONオブジェクトのプロパティにアクセスする場合などは、文字列リテラルを使ってアクセスする必要があります。

一貫した方法でプロパティにアクセスするためには、以下のような点に気を付けます。

  • オブジェクトがコンパイルされたコードの中でだけ生成・利用される場合は、ドットシンタックスを使う
  • サーバからのレスポンスなど外部で定義されるプロパティを使う場合は、文字列リテラルを使う
  • obj.hasOwnProperty()は使わない

コンパイルされたコードに外部からアクセスする

jsファイルを読み込んだあと、別の箇所でその機能を利用したいケースがあります。 例えば、次の例は普段は特に問題なく動作します。

HTMLファイル(の一部) jsファイル

このjsファイルをADVANCEDコンパイルしたものが以下です。

HTMLで読み込むjsファイルをコンパイル後のファイルに差し替えると、2つ目のscriptタグで実行しているhello();が動かなくなってしまいます。helloという関数はaという名前にリネームされており、既に存在しないためです。このような場合は、グローバル空間に名前を「公開」します。

コンパイル前 コンパイル後

相変わらずhelloはaにリネームされますが、aをhelloという名前でwindowオブジェクトに公開することで、外部のscriptタグからもhello関数が利用できるようになりました。

また、外部から利用する目的のオブジェクトは、コンパイル対象のコード内ではそれを利用しないため、到達不能コードと判断されて除去されてしまいます。このような「必要なコードが到達不能と判断されて除去されてしまう問題」に対しても、目的のオブジェクトを公開することで防ぐことができます。

Closure Libraryには、goog.exportSymbol()という関数が用意されており、任意の名前空間で公開できるようになっています。

サードパーティライブラリのADVANCEDコンパイル対応

ADVANCEDコンパイル対応の一番の難関が、サードパーティライブラリをどうするかという問題です。 kintoneでは、サードパーティライブラリとしてHighchartsと、Highchartsが依存するjQueryを利用しています。これらのライブラリも、kintoneのjsとまとめてADVANCEDコンパイルしています。

基本的に、サードパーティライブラリはClosure Compilerのコンパイルセーフな書き方を完全に無視した記述で書かれているので困ったものです。最初はexportSymbolを使ってjQueryの$オブジェクトを公開すれば普通に使えるんじゃないかと思って試してみたんですが、ADVANCEDコンパイルするとクロージャで閉じた空間の変数・関数もがっつりリネームされてしまい全然動きません\(^o^)/

最終的にとった方法が、externs宣言とexportSymbolを駆使してライブラリで使われるシンボルをすべてリネームから保護するというものです。

externs宣言とは

Closure Compilerでは、externsというオプションでjsファイルを指定すると、そのjs内に記述されたシンボルをリネーム対象から外すという機能が提供されています。ECMAScriptやDOMインタフェースなど、組み込みの「リネームしてはいけないシンボル」も、この仕組みで実現されています。 デフォルトで使われるexterns宣言は、こちらで確認することができます。

ためしに次のコードを、externs宣言を使ってADVANCED対応してみましょう。

コンパイル前 コンパイル後

オブジェクトのプロパティに文字列でアクセスしているので動きませんね。 ここで、次のexterns宣言を記述したjsファイルを作成します。

JSDocが記述されていると、Closure Compilerがそれに基づいた型チェックをしてくれるようになりますが、必須ではありません。 コンパイル時にこのjsファイルをexternsオプションで指定すると、コンパイル結果が以下のように変化します。

変数objとlocationUriプロパティがリネームされなくなりました!このように、externs宣言を使うと、元のコードに手を加えずにADVANCEDコンパイル対応ができるようになります。コンパイル対応のためにライブラリのコードに手を加えると、バージョンアップが困難になるなど多くの問題が発生するので、手を加えずに済むというのはとても助かります。ただし、このexterns宣言の作成がかなり面倒です。幸いなことに、Closure Compilerのリポジトリには有名どころのライブラリのexterns集があり、こちらを利用することができます。完璧に対応できている保証はありませんが、externs作成のかなりの負担を軽減できると思います。

ライセンス表記の保護

最後に、ADVANCEDコンパイルとライセンス表記の保護について触れておきます。配布されているライブラリはコメントにライセンス情報が記されています。jsのコンパイルでこの情報が削除されてはまずいので、Closure CompilerはJSDocの@licenseタグのついたコメントを削除しないようになっています。詳細はこちらを参照してください。

コンパイル時のコードチェック

Closure Compilerは、コンパイル時にコードを解析してバグの原因になりうる箇所に対して警告を表示するという素晴らしい機能も持っています。この機能をコードチェックツールとして活用することで、バグの出にくいコードを書くことができます。実際のやり方はWEB+DB PRESS Vol.70で詳しく解説していますので、興味のある方はぜひこちらもご覧ください。

まとめ

Closure CompilerのADVANCEDコンパイルに対応するには、基本的に以下の方法で実現できます。

  • 一貫した方法でプロパティにアクセスする
  • 必要に応じてオブジェクトを公開する
  • externs宣言を作成してサードパーティライブラリをリネームから保護する

こういった地道なADVANCEDコンパイル対応の経験から、僕はJavaScriptのライブラリを読むときは「これADVANCEDコンパイルできるかな?」という視点で見てしまうようになりました。しかも、大体のコードはできないので見ていて胸焼けがしてきます。いやな職業病を患いました。

それでも、ADVANCEDモードの圧縮率は非常に魅力的ですし、トライする価値のある部分だと思います。みなさんもぜひ、この速さを体感してみてください!