高度なコンパイル

概要

Closure Compiler で compilation_levelADVANCED_OPTIMIZATIONS に使うと、SIMPLE_OPTIMIZATIONS または WHITESPACE_ONLY でコンパイルするよりも圧縮率が向上します。ADVANCED_OPTIMIZATIONS でコンパイルすると、コードの変換やシンボル名の変更をより積極的に行うことで、追加の圧縮を実現できます。ただし、この積極的なアプローチでは、ADVANCED_OPTIMIZATIONS を使用する際には、出力コードが入力コードと同じように機能するように、注意する必要があります。

このチュートリアルでは、ADVANCED_OPTIMIZATIONS のコンパイル レベルと、ADVANCED_OPTIMIZATIONS でコンパイルした後にコードを動作させるためにできることについて説明します。また、コンパイラによって処理されるコードの外部コードで定義されたシンボルである extern のコンセプトも導入されています。

このチュートリアルを読む前に、Closure Compiler ツール(コンパイラ サービス UIコンパイラ サービス API、またはコンパイラ アプリケーション)を使って JavaScript をコンパイルするプロセスを理解しておく必要があります。

用語に関する注意: --compilation_level コマンドライン フラグは、よく使用される略語 ADVANCEDSIMPLE をサポートしています。また、より正確な ADVANCED_OPTIMIZATIONSSIMPLE_OPTIMIZATIONS もサポートしています。このドキュメントでは長い形式を使用しますが、名前はコマンドラインで相互に使用できます。

  1. さらに優れた圧縮
  2. ADVANCED_OPTIMIZATIONS を有効にする方法
  3. ADVANCED_OPTIMIZATIONS を使用する際の注意点
    1. 残しておきたいコードの削除
    2. プロパティ名に一貫性がない
    3. 2 つのコード部分のコンパイル
    4. コンパイル済みコードと非コンパイル済みコードの参照の破損

圧縮効率が向上

Closure Compiler のデフォルトのコンパイル レベルは SIMPLE_OPTIMIZATIONS であり、ローカル変数の名前を変更することで JavaScript のサイズを小さくできます。ただし、ローカル変数以外にも短縮できるシンボルがあります。また、シンボル名を変更する以外にコードを圧縮する方法もあります。ADVANCED_OPTIMIZATIONS によるコンパイルは、コード圧縮の可能性をすべて利用します。

次のコードについて、SIMPLE_OPTIMIZATIONSADVANCED_OPTIMIZATIONS の出力を比較します。

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

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

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

SIMPLE_OPTIMIZATIONS でコンパイルすると、コードが次のように短縮されます。

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

ADVANCED_OPTIMIZATIONS でコンパイルすると、コードが次のように完全に短縮されます。

alert("Flowers");

どちらのスクリプトも "Flowers" を読み取るアラートを生成しますが、2 番目のスクリプトははるかに小さくなります。

ADVANCED_OPTIMIZATIONS レベルは、変数名の簡単な短縮にとどまらず、次のような複数の手段を提供します。

  • 名前の変更:

    SIMPLE_OPTIMIZATIONS でコンパイルすると、displayNoteTitle() 関数と unusedFunction() 関数の note パラメータの名前のみが変更されます。これはスクリプト内で関数にローカルな唯一の変数だからです。ADVANCED_OPTIMIZATIONS は、グローバル変数 flowerNote の名前も変更します。

  • 不要なコードの削除:

    ADVANCED_OPTIMIZATIONS でコンパイルすると、コード内で呼び出されることはないため、関数 unusedFunction() は完全に削除されます。

  • 関数のインライン化:

    ADVANCED_OPTIMIZATIONS でコンパイルすると、displayNoteTitle() の呼び出しが、関数の本体を構成する単一の alert() に置き換えられます。関数呼び出しを関数の本体に置き換えることを「インライン」と呼びます。関数が複雑であったり複雑だったりすると、インライン関数によってコードの動作が変わる可能性がありますが、Closure Compiler は、このケースは安全でスペースを節約できると判断します。ADVANCED_OPTIMIZATIONS でコンパイルすると、安全に行えると判断された場合、定数と一部の変数がインライン化されます。

このリストは、ADVANCED_OPTIMIZATIONS コンパイルで実行できるサイズ削減のための変換の一例にすぎません。

ADVANCED_OPTIMIZATIONS を有効にする方法

Closure Compiler サービスの UI、サービス API、アプリには、compilation_levelADVANCED_OPTIMIZATIONS に設定するさまざまなメソッドがあります。

Closure Compiler サービス UI で ADVANCED_OPTIMIZATIONS を有効にする方法

Closure Compiler サービス UI で ADVANCED_OPTIMIZATIONS を有効にするには、[Advanced] ラジオボタンをクリックします。

Closure Compiler Service API で ADVANCED_OPTIMIZATIONS を有効にする方法

Closure Compiler Service API で ADVANCED_OPTIMIZATIONS を有効にするには、次の Python プログラムのように、compilation_levelADVANCED_OPTIMIZATIONS として、リクエスト パラメータを含めます。

#!/usr/bin/python2.4

import httplib, urllib, sys

params = urllib.urlencode([
    ('code_url', sys.argv[1]),
    ('compilation_level', 'ADVANCED_OPTIMIZATIONS'),
    ('output_format', 'text'),
    ('output_info', 'compiled_code'),
  ])

headers = { "Content-type": "application/x-www-form-urlencoded" }
conn = httplib.HTTPSConnection('closure-compiler.appspot.com')
conn.request('POST', '/compile', params, headers)
response = conn.getresponse()
data = response.read()
print data
conn.close()

Closure Compiler アプリで ADVANCED_OPTIMIZATIONS を有効にする方法

Closure Compiler アプリで ADVANCED_OPTIMIZATIONS を有効にするには、次のコマンドにコマンドライン フラグ --compilation_level ADVANCED_OPTIMIZATIONS を指定します。

java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js hello.js

ADVANCED_OPTIMIZATIONS を使用する際の注意点

ADVANCED_OPTIMIZATIONS の一般的な意図しない影響と、それらを回避するための手順を以下に示します。

必要なコードの削除

以下の関数のみを ADVANCED_OPTIMIZATIONS でコンパイルすると、Closure Compiler は空の出力を生成します。

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

コンパイラに渡す JavaScript では関数が呼び出されないため、Closure コンパイラはこのコードが不要であると見なします。

多くの場合、この動作はまさに希望どおりです。たとえば、大規模なライブラリと一緒にコードをコンパイルする場合、Closure Compiler は、そのライブラリから実際に使用している関数を特定して、使用しない関数を破棄できます。

ただし、必要な関数を Closure Compiler が削除している場合は、次の 2 つの方法で防止できます。

  • 関数呼び出しを、Closure Compiler で処理されたコードに移動します。
  • 公開する関数に外部関数を含めます。

以降のセクションでは、各オプションについて詳しく説明します。

解決策: クロージャ コンパイラによって処理されるコードに関数呼び出しを移動する

Closure Compiler を使用してコードの一部のみをコンパイルすると、不要なコードが削除されることがあります。たとえば、関数定義のみを含むライブラリ ファイルと、ライブラリを含む HTML ファイルに、それらの関数を呼び出すコードが含まれているとします。この場合、ADVANCED_OPTIMIZATIONS でライブラリ ファイルをコンパイルすると、Closure Compiler はすべてのライブラリ関数を削除します。

この問題の最も簡単な方法は、関数を呼び出すプログラムの部分と共同で関数をコンパイルすることです。たとえば、Closure Compiler は次のプログラムをコンパイルする際に displayNoteTitle() を削除しません。

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
displayNoteTitle({'myTitle': 'Flowers'});

この場合、Closure Compiler は呼び出されたものを認識するため、displayNoteTitle() 関数は削除されません。

つまり、Closure Compiler に渡すコードにプログラムのエントリ ポイントを含めることで、不要なコードの削除を防止できます。プログラムのエントリ ポイントは、プログラムの実行を開始するコード内の場所です。たとえば、前のセクションのフラワーノート プログラムでは、最後の 3 行はブラウザに JavaScript が読み込まれるとすぐに実行されます。これは、このプログラムのエントリ ポイントです。必要なコードを決定するために、Closure Compiler はこのエントリ ポイントから開始し、そこからプログラムの制御フローをトレースします。

解決策: 公開したい関数の Externs を含める

このソリューションの詳細については、以下外部とエクスポートに関するページをご覧ください。

プロパティ名の不一致

クロージャ コンパイラのコンパイルでは、コード内の文字列リテラルが変更されることはありません。使用するコンパイル レベルも関係ありません。つまり、ADVANCED_OPTIMIZATIONS によるコンパイルでは、コードが文字列にアクセスするかどうかによってプロパティの処理方法が異なります。プロパティへの文字列参照とドット構文の参照が混在している場合、Closure Compiler ではプロパティに対する一部の参照のみが名前変更されます。その結果、コードが正しく実行されていない可能性があります。

たとえば、次のコードを見てみましょう。

function displayNoteTitle(note) {
  alert(note['myTitle']);
}
var flowerNote = {};
flowerNote.myTitle = 'Flowers';

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

このソースコードの最後の 2 つのステートメントは、まったく同じ処理を行います。ただし、ADVANCED_OPTIMIZATIONS でコードを圧縮すると、次のようになります。

var a={};a.a="Flowers";alert(a.a);alert(a.myTitle);

圧縮コードの最後のステートメントでエラーが発生します。myTitle プロパティへの直接参照の名前を a に変更しましたが、displayNoteTitle 関数内での myTitle 引用符付きの参照は変更していません。その結果、最後のステートメントは、存在しなくなった myTitle プロパティを参照します。

解決方法: プロパティ名に一貫性を持たせる

このソリューションはとてもシンプルです。任意の型またはオブジェクトでは、ドット構文または引用符で囲まれた文字列のみを使用します。特に同じプロパティを参照する場合は、構文を混在させないでください。

また、可能な場合はチェック構文を最適化し、ドット構文を使用することをおすすめします。引用符付きの文字列プロパティへのアクセスは、デコードされた JSON など、名前が外部ソースから取得される場合など、Closure Compiler で名前の変更を行わない場合にのみ使用します。

2 つのコード部分のコンパイル

アプリケーションを複数のコードに分割する場合は、チャンクを個別にコンパイルすることをおすすめします。ただし、2 つのコードチャンクが相互作用すると、問題が発生する可能性があります。成功した場合でも、2 つの Closure Compiler 実行の出力には互換性がありません。

たとえば、アプリケーションが 2 つの部分(データを取得する部分とデータを表示する部分)に分割されているとします。

データを取得するコードを次に示します。

function getData() {
  // In an actual project, this data would be retrieved from the server.
  return {title: 'Flower Care', text: 'Flowers need water.'};
}

データを表示するためのコードを次に示します。

var displayElement = document.getElementById('display');
function displayData(parent, data) {
  var textElement = document.createTextNode(data.text);
  parent.appendChild(textElement);
}
displayData(displayElement, getData());

この 2 つのコードチャンクを個別にコンパイルしようとすると、いくつかの問題に直面します。まず、Closure コンパイラが保持するコードの削除で説明されている理由により、getData() 関数を削除します。2 番目に、Closure Compiler は、データを表示するコードの処理時に致命的なエラーを生成します。

input:6: ERROR - variable getData is undefined
displayData(displayElement, getData());

コンパイラは、データを表示するコードをコンパイルするときに getData() 関数にアクセスできないため、getData を未定義として扱います。

解決方法: ページのすべてのコードをまとめる

コンパイルが適切に行われるようにするには、1 回のコンパイルでページのすべてのコードをコンパイルします。Closure Compiler は複数の JavaScript ファイルと JavaScript 文字列を入力として受け入れることができるので、1 つのコンパイル リクエストでライブラリのコードや他のコードを一緒に渡すことができます。

注: この方法は、コンパイルされたコードとコンパイルされていないコードを混在させる必要がある場合、機能しません。この状況に対処するためのヒントについては、コンパイル済みコードと非コンパイル済みコードの間の参照の破損をご覧ください。

コンパイル済みコードと非コンパイル済みコードの参照の破損

ADVANCED_OPTIMIZATIONS のシンボル名を変更すると、Closure Compiler によって処理されるコードとその他のコード間の通信ができなくなります。コンパイルにより、ソースコードで定義されている関数の名前が変更されます。関数を呼び出す外部コードは、引き続き古い関数名を参照するため、コンパイル後に破損します。同様に、コンパイル済みコード内で外部定義されたシンボルへの参照は、Closure Compiler によって変更される場合があります。

「コンパイルされていないコード」には、eval() 関数に文字列として渡されるコードが含まれることに注意してください。Closure コンパイラはコード内の文字列リテラルを変更することはないため、Closure コンパイラは eval() ステートメントに渡された文字列を変更しません。

これらに関連する問題がありますが、コンパイルされた外部との通信が維持され、外部からコンパイルされた通信が維持されます。これらの問題には共通の解決策がありますが、それぞれの微妙な違いがあります。Closure Compiler を最大限に活用するには、どのケースに該当するかを理解することが重要です。

先に進む前に、外部とエクスポートについて理解しておくことをおすすめします。

コンパイル済みコードから外部コードを呼び出すソリューション: Externs でコンパイルする

別のスクリプトによってページに渡されるコードを使用する場合は、Closure Compiler がその外部ライブラリで定義されているシンボルへの参照を変更しないようにする必要があります。これを行うには、外部ライブラリの外部変数を含むファイルをコンパイルに追加します。これにより Closure Compiler に、制御できない名前があるため、変更できなくなります。コードには、外部ファイルと同じ名前を使用する必要があります。

この一般的な例として、OpenSocial APIGoogle Maps API などの API があります。たとえば、適切な外部変数を指定せずにコードが OpenSocial 関数 opensocial.newDataRequest() を呼び出すと、Closure Compiler はこの呼び出しを a.b() に変換します。

外部コードからコンパイルされたコードを呼び出すソリューション: Externs の実装

ライブラリとして再利用する JavaScript コードがある場合は、Closure Compiler を使用してライブラリのみを圧縮しながら、コンパイルされていないコードもライブラリ内の関数を呼び出すことができます。

この状況での解決策は、ライブラリのパブリック API を定義する一連の exten を実装することです。コードでは、これらの外部宣言で宣言されたシンボルの定義を指定します。これは、外部が言及しているクラスまたは関数を意味します。また、外部で宣言されたインターフェースをクラスに実装させることもできます。

これらの外部変数は、自分自身だけでなく、他のユーザーにとっても有用です。ライブラリのコンシューマは、コードのコンパイル時にこれらのライブラリをインクルードする必要があります。これは、ライブラリが外部スクリプトの観点から見たスクリプトであるためです。外部要因は、お客様と消費者との間の契約だと考えてください。どちらもコピーが必要です。

そのため、コードをコンパイルするときには、外部にも同じものを含めてください。通常、外部要素は「他のどこかから来る」と考えられているため、これは異常に感じられるかもしれませんが、公開するシンボルの名前を Closure Compiler に伝える必要があります。そのため、名前が変更されることはありません。

重要な注意点の 1 つは、外部シンボルを定義するコードについて「重複定義」の診断情報が届く可能性があることです。クロージャ コンパイラは、外部内のシンボルが外部ライブラリから提供されていると想定しており、現在、定義が意図的に指定されているかどうかを把握できません。 これらの診断は、抑制しても問題ありません。抑制は、API を実際に実行していることを確認するものと考えることができます。

また、Closure Compiler は、定義が外部宣言の型と一致することを型チェックします。これにより、定義が正しいことを確認しやすくなります。