高度なコンパイル

概要

ADVANCED_OPTIMIZATIONScompilation_level で Closure Compiler を使用すると、SIMPLE_OPTIMIZATIONS または WHITESPACE_ONLY でのコンパイルよりも圧縮率が向上します。ADVANCED_OPTIMIZATIONS を使用したコンパイルでは、コードの変換とシンボルの名前変更をより積極的に行うことで、圧縮率を高めています。ただし、このより積極的なアプローチでは、ADVANCED_OPTIMIZATIONS を使用する際に、出力コードが入力コードと同じように動作するように、より注意を払う必要があります。

このチュートリアルでは、ADVANCED_OPTIMIZATIONS コンパイル レベルの機能と、ADVANCED_OPTIMIZATIONS でコンパイルした後にコードが動作するようにするための方法について説明します。また、extern の概念も紹介します。これは、コンパイラによって処理されるコードの外部のコードで定義されるシンボルです。

このチュートリアルを読む前に、Java ベースのコンパイラ アプリケーションなど、Closure Compiler ツールのいずれかを使用して JavaScript をコンパイルするプロセスを理解しておく必要があります。

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

  1. Even Better Compression
  2. ADVANCED_OPTIMIZATIONS を有効にする方法
  3. ADVANCED_OPTIMIZATIONS を使用する際の注意点
    1. 削除したくないコードの削除
    2. プロパティ名が一致しない
    3. コードの 2 つの部分を個別にコンパイルする
    4. コンパイル済みコードとコンパイルされていないコード間の参照の破損

圧縮率の向上

デフォルトのコンパイル レベル SIMPLE_OPTIMIZATIONS を使用すると、Closure Compiler はローカル変数の名前を変更して 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 アプリケーションで 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 Compiler はこのコードは不要であると想定します。

多くの場合、この動作はユーザーが望むものです。たとえば、コードを大きなライブラリと一緒にコンパイルする場合、Closure Compiler は、そのライブラリのどの関数が実際に使用されているかを判断し、使用されていない関数を破棄できます。

ただし、Closure Compiler が保持したい関数を削除していることが判明した場合は、次の 2 つの方法でこれを防ぐことができます。

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

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

解決策: 関数呼び出しを 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 に渡すコードにプログラムのエントリ ポイントを含めることで、不要なコードの削除を防ぐことができます。プログラムのエントリ ポイントは、プログラムの実行が開始されるコード内の場所です。たとえば、前のセクションの花柄のメモ プログラムでは、JavaScript がブラウザに読み込まれるとすぐに最後の 3 行が実行されます。これがこのプログラムのエントリ ポイントです。保持する必要があるコードを特定するために、Closure Compiler はこのエントリ ポイントから開始し、そこからプログラムの制御フローを前方へトレースします。

解決策: 公開する関数の Externs を含める

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

一貫性のないプロパティ名

Closure Compiler のコンパイルでは、使用するコンパイル レベルに関係なく、コード内の文字列リテラルが変更されることはありません。つまり、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 Compiler は、保持したいコードの削除で説明した理由により、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 Compiler はコード内の文字列リテラルを変更しないため、Closure Compiler は eval() ステートメントに渡される文字列を変更しません。

コンパイル済みから外部への通信の維持と、外部からコンパイル済みへの通信の維持は、関連しているものの異なる問題であることに注意してください。これらの個別の問題には共通の解決策がありますが、それぞれにニュアンスがあります。Closure Compiler を最大限に活用するには、どのケースに該当するかを理解することが重要です。

続行する前に、extern と export について理解しておくとよいでしょう。

コンパイル済みコードから外部コードを呼び出すためのソリューション: 外部ファイルを使用したコンパイル

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

この例としては、OpenSocial APIGoogle Maps API などの API が一般的です。たとえば、コードが OpenSocial 関数 opensocial.newDataRequest() を呼び出す場合、適切な extern がないと、Closure Compiler はこの呼び出しを a.b() に変換します。

外部コードからコンパイル済みコードを呼び出すためのソリューション: extern の実装

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

この状況での解決策は、ライブラリの公開 API を定義する一連の extern を実装することです。コードでは、これらの extern で宣言されたシンボルの定義を指定します。つまり、externs で言及されているクラスまたは関数です。また、クラスで externs で宣言されたインターフェースを実装することも意味します。

これらの extern は、自分だけでなく他のユーザーにも役立ちます。ライブラリの利用者は、コードをコンパイルする際にそれらを含める必要があります。ライブラリは利用者にとって外部スクリプトであるためです。extern は、あなたとコンシューマー間の契約と考えることができます。両方にコピーが必要です。

そのため、コードをコンパイルするときに、コンパイルに extern も含めるようにしてください。外部変数は「どこかから来たもの」と考えることが多いので、これは奇妙に思えるかもしれませんが、公開するシンボルを Closure Compiler に伝えることで、シンボルが名前変更されないようにするために必要です。

ここで重要な注意点として、extern シンボルを定義するコードについて「重複定義」の診断結果が表示されることがあります。Closure Compiler は、externs 内のシンボルは外部ライブラリから提供されるものと想定しており、現在、定義を意図的に提供していることを理解できません。これらの診断は安全に抑制できます。抑制は、API を実際に満たしていることを確認する手段と考えることができます。

また、Closure Compiler は、定義が extern 宣言の型と一致するかどうかを型チェックする場合があります。これにより、定義が正しいことを確認できます。