Closure Compiler の ADVANCED_OPTIMIZATIONS の使い方

  • 21
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Closure Compiler とは

Closure Compiler とは、 Google が開発している JavaScript 最適化ツールの一つです。
似たようなツールとしては YUI CompressorUglifyJS などが挙げられます。

Closure Compiler が他のツールと一線を画している部分は、大胆な変更を伴うアグレッシブな最適化が行えることに尽きます。
その最適化効果は絶大ですが、単純に適用するだけでは 動かないコード になってしまう可能性が大きいです。
Closure Compiler の恩恵を最大限に受けるためには、いくつかの ルールに従ったコードを記述する 必要があります。

ここでは、 Closure Compiler の概要を説明したあと、その恩恵を最大限に受けるための使い方を説明していきたいと思います。

Compilation Level

Closure Compiler は、最適化をする際にどの程度の最適化を行うのかを指定することができます。
最適化レベルは3種類存在していて、例として以下のコードをそれぞれのレベルで最適化を行った場合のコードを示します。

sample.js
function unusedFunction(note) {
  alert(note['text']);
}

function displayNoteTitle(note) {
  alert(note['title']);
}

var flowerNote = {};
flowerNote['title'] = "Flowers";
displayNoteTitle(flowerNote);

WHITESPACE_ONLY

WHITESPACE_ONLY はその名が示す通り、空白文字のみを除去します。
空白を除去しているだけなので最適化しているかと言われると疑問ですね。

WHITESPACE_ONLY
function unusedFunction(note){alert(note["text"])}function displayNoteTitle(note){alert(note["title"])}var flowerNote={};flowerNote["title"]="Flowers";displayNoteTitle(flowerNote);

特に JavaScript の動作に影響はありません。空白を除去しただけなので当然とも言えます。

SIMPLE_OPTIMIZATIONS

次に、 SIMPLE_OPTIMIZATIONS ですが、これは空白除去に加え、動作に影響のないレベルで変数名の変更などを行ってくれます。

SIMPLE_OPTIMIZATIONS
function unusedFunction(a){alert(a.text)}function displayNoteTitle(a){alert(a.title)}var flowerNote={title:"Flowers"};displayNoteTitle(flowerNote);

関数の引数名が a になっていたり、プロパティアクセスが ['title'] から .title に変わっていたりしてます。
このあたりまでは多少のコードの違いはありますが、 YUI Compressor や UglifyJS などでも行われている最適化です。

ADVANCED_OPTIMIZATIONS

この ADVANCED_OPTIMIZATIONS こそが Closure Compiler の最大の特徴であり、 最も大きな最適化効果が期待できます
その効果は、以下の最適化後のコードを見れば効果は一目瞭然だと思います。

ADVANCED_OPTIMIZATIONS
alert("Flowers");

確かに、よくコードを読んでみるとこれしかやっていないです。
使われていない関数は必要ないし、そもそもオブジェクトを作る必要すらない、 alert() 呼んでいるだけなら関数もいらんだろインライン化しちまえ、というわけです。

しかし、 これだけの最適化を行えば変なバグが入り込みそうだなあ、というのは感じられると思います。
特に、使っていない関数が勝手に消されてしまうのは困る場合が多いでしょう。
このファイルではその関数を使っていないのかもしれませんが、別のファイルでは使われている可能性があるからです。

ADVANCED_OPTIMIZATION を利用するためのコーディング

前述した通り、 ADVANCED_OPTIMIZATION は非常に強力な最適化を行ってくれますが、それ故にコードがまともに動かなくなってしまう危険性を孕んでいます。

ですので、 Google は安全に ADVANCED_OPTIMIZATION を適用するための注意点を解説しています。
ここからはそれらの問題点とその解決策について説明していきます。

呼び出されない関数の削除

例えば、以下のコードを ADVANCED_OPTIMIZATION を指定して最適化をかけた場合、 Closure Compiler は何も出力しません。

sample2.js
function unusedFunction(note) {
  alert(note['text']);
}

なぜなら、この関数は渡された JavaScript コード内では一度も呼び出されていないからです。
このようなコードは、 Closure Compiler では必要のないコードと判断します。
このコードが削除されないようにするためには、以下の2つの方法があります。

  • 引き渡すコード内で1度でも呼び出す
  • キープしたいシンボルをエクスポートする

引き渡すコード内で1度でも呼び出す

これは単純な解決策です。
呼ばれていないから削除されてしまうのだから、呼べばいいだけです。
最適化する JavaScript は2つ以上指定することもできるので、呼んでいる側と呼ばれている側の両方のファイルを同時に指定する、といった解決策もあります。

キープしたいシンボルをエクスポートする

呼び出せばいいなどと言われても、ライブラリとして提供するつもりだった場合などは、呼び出すのはそのライブラリを利用する側なので、そもそも呼び出す必要がありません。
そういう場合はこちらの方法を使います。

sample2ex.js
function unusedFunction(note) {
  alert(note['text']);
}
window['unusedFunction'] = unusedFunction;

window オブジェクトに ブラケットシンタックス で代入しておくという方法です。
このようなコードの場合は、最適化をかけると以下のようになります。

sample2ex-adv.js
window.unusedFunction=function(a){alert(a.text)};

window.unusedFunction に代入するコードになりました。
ブラウザでは window オブジェクトのプロパティは明示的に指定しなくても使えるため、もともと使いたかったような使い方が可能です。

また、コンストラクタ関数やそのプロトタイププロパティをエクスポートしたい場合は以下のようになります。

sample3.js
/**
 * @constructor
 * @param {string} name
 */
var MyClass = function(name) {
  this.myName = name;
};

/**
 * alert my name.
 */
MyClass.prototype.myMethod = function() {
  alert(this.myName);
};

window['MyClass'] = MyClass; // <-- Constructor
MyClass.prototype['myMethod'] = MyClass.prototype.myMethod;
sample3-adv.js
function a(b){this.b=b}a.prototype.a=function(){alert(this.b)};window.MyClass=a;a.prototype.myMethod=a.prototype.a;

コンストラクタ関数を定義した場合は、必ず JsDoc スタイルのコメントでコンストラクタ関数であることを明示する 必要があります。明示していない場合 Closure Compiler は MyClass を普通の関数だと解釈して処理しようとしてしまいます。ところが、関数内に this があるため、これがグローバルオブジェクトにアクセスしているものと勘違いしてエラーを吐いてしまいます。

このあたりで気づいたかもしれませんが、サンプルのコンストラクタ関数はメソッドが一つしかないから良いですが、メソッドが増えてくるといちいちエクスポートするのが面倒になるのは確定的に明らかです。
Google もそれはわかっているようで、 Closure Library 関数の goog.exportSymbol()goog.exportProperty() を使うと良いよ、と言っています。

プロパティ名の矛盾

Closure Compiler は、文字列リテラルを変更しません。これは ADVANCED_OPTIMIZATIONS に限らず、どの最適化レベルでも同じです。

しかし、ドットシンタックスでアクセスしているプロパティの名前は、短い名前へと変更します。問題は、オブジェクトのプロパティへのアクセスを、ブラケットシンタックスとドットシンタックスの2つを混ぜて利用してしまった場合に起こります。

sample4.js
function displayNoteTitle(note) {
  alert(note['myTitle']); // ここはブラケットだが…
}
var flowerNote = {};
flowerNote.myTitle = 'Flowers'; // ここではドットだ!

alert(flowerNote.myTitle);
displayNoteTitle(flowerNote);

このコードを ADVANCED_OPTIMIZATIONS で最適化すると以下のようになってしまいます。

sample4-adv.js
var a={a:"Flowers"};alert(a.a);alert(a.myTitle); // a.myTitle がない!

この問題を避けるために、プロパティへのアクセスは極力ドットシンタックスを利用するようにしましょう。
ブラケットシンタックスを利用するのは、シンボルをエクスポートする場合など、勝手に名前を変えられては困る場合のみにしておくことで、プロパティ名の矛盾が起こることを防ぎます。

最適化コードとそうでないコードの相互参照の破壊

Closure Compiler で最適化したコードは、関数定義などの名前がリネームされます。
もし、これらの関数を外部のコードから呼び出していた場合は、名前が一致しなくなりコードは動かなくなってしまうことでしょう。
この問題については、2つのパターンがあります。

  • 最適化コードを外部から呼び出す場合

こちらはライブラリなどを提供したい場合に発生します。
外部で利用してもらうはずのコードが削除されてしまったり、リネームされてしまったりといった問題が起こります。
この問題については前述したとおり、変えられては困るシンボルをエクスポートすれば解決できます。

  • 外部 API を最適化コードから呼び出す場合

こちらはサードパーティ製のライブラリなどを利用している場合に発生します。
サードパーティのライブラリの API を呼び出しているつもりが、 Closure Compiler によってその呼び出しコードが書き換えられてしまうパターンです。

例えば、 OpenSocial JavaScript の関数を呼び出すコード opensocial.newDataRequest() を書いていたのに、これが a.b() に書き換えられてしまった、などということが起こります。

Closure Compiler はこのような場合について、外部のコードであることを示すためのメカニズムを用意しています。

外部シンボルの宣言

例えば、以下のようなコードがあったとします。

sample5.js
/**
 * A simple script for adding a list of notes to a page. The list diplays
 * the text of each note under its title.
 */

/**
 * Creates the DOM structure for a note and adds it to the document.
 */
function makeNoteDom(noteTitle, noteContent, noteContainer) {
  // Create DOM structure to represent the note.
  var headerElement = lib.textDiv(noteTitle);
  var contentElement = lib.textDiv(noteContent);

  var newNote = document.createElement('div');
  newNote.appendChild(headerElement);
  newNote.appendChild(contentElement);

  // Add the note's DOM structure to the document.
  noteContainer.appendChild(newNote);
}

/**
 * Iterates over a list of note data objects and creates a DOM
 */
function makeNotes(data, noteContainer) {
  for (var i = 0; i < data.length; i++) {
    makeNoteDom(data[i].title, data[i].content, noteContainer);
  }
}

function main() {
  var noteData = [
    { title: 'Note 1', content: 'Content of Note 1' },
    { title: 'Note 2', content: 'Content of Note 2' }
  ];
  var noteListElement = document.getElementById('notes');
  makeNotes(noteData, noteListElement);
}

main();

11-12 行目の lib.textDiv() が外部ライブラリの API です。
このコードをこのまま最適化すると以下のようになってしまいます。

sample5-adv-miss.js
for(var a=[{title:"Note 1",content:"Content of Note 1"},{title:"Note 2",content:"Content of Note 2"}],b=document.getElementById("notes"),c=0;c<a.length;c++){var d=a[c].content,e=b,
f=lib.a(a[c].title),
g=lib.a(d),
h=document.createElement("div");h.appendChild(f);h.appendChild(g);e.appendChild(h)};
// 実際には改行なし

lib.textDiv()lib.a() に書き換わってしまっています。
lib ライブラリの提供者がイカれたインターフェースを用意するアホでなければ、 a などというプロパティは存在せず、エラーになることでしょう。

この問題を避けるためには、 --externs オプションを利用して、外部関数宣言を指定します。

sample5-externs.js
var lib = {
  textDiv: true
};

上記のようなファイルを用意した上で、 --externs オプションを指定して最適化を行います。
そうすれば、 lib.textDiv() が書き換わったりすることなく出力されます。

sample5-adv.js
for(var a=[{title:"Note 1",content:"Content of Note 1"},{title:"Note 2",content:"Content of Note 2"}],b=document.getElementById("notes"),c=0;c<a.length;c++){var d=a[c].content,e=b,
f=lib.textDiv(a[c].title),
g=lib.textDiv(d),
h=document.createElement("div");h.appendChild(f);h.appendChild(g);e.appendChild(h)};

--externs に使用しているライブラリをそのまま渡してしまうこともできます。まあ、いちいち専用ファイルを用意するのも面倒なのでこちらが主流になるでしょう。

その他の手段としては、 使っているライブラリなども一緒に最適化してしまうという方法もあります。
こちらは MIT ライセンスなど改変可能なライブラリであれば、使っていない API が削除されるためかなりの最適化効果を見込むことができます。
しかし、そのライブラリが Closure Compiler の制約に則ったコードを書いている保証はないため、危険性はかなり高いです。おそらく、ほぼ失敗するでしょう。

まとめ

以上が Closure Compiler の ADVANCED_OPTIMIZATIONS で最適化を行う場合の問題点とその解決策です。
要点だけ抜き取ると以下の通りです。

  • Closure Compiler は ADVANCED_OPTIMIZATIONS による最適化がかなり高度でアグレッシブな改変を行う
  • ADVANCED_OPTIMIZATIONS を利用するために
    • 削除やリネームされると困るシンボルはエクスポートする
    • プロパティアクセスにブラケットシンタックスとドットシンタックスを混ぜない
    • 外部 API を利用する場合は --externs オプションを使う

これらの注意点を念頭に置いて、ちょっぱや JavaScript を書くことに挑戦してみましょう!


このエントリは Google 本家ガイド Advanced Compilation and Externs を参考に書きましたが、本家ガイドに書いてある内容と、実際に最新版の Closure Compiler で最適化してみた結果が違っていました。
そのため、本家ガイドに書いてある注意事項でも最新版では問題になっていない場合が見受けられたため、そこは省略しています。