JavaScript の圧縮

2007/11/14 追記:より包括的な「JavaScript ファイルの圧縮・再訪 - daily dayflower」も書きました。

亜細亜ノ蛾さんの報告にもある通り,gzip 圧縮した JavaScript ファイルをおいとけばブラウザがきちんと読み込んでくれる。odz さんのところの議論によると Safari でも Content-Type を適切に定義すればオッケーぽい。


…………あたりのことを知らなかったゆえ JavaScript 圧縮機について調べてました。

  • Huffman JavaScript Compression
    • 方式: 文字単位のハフマン符号化(推定)
    • 符号表の格納とバイナリデータの文字列化(4/3倍)があるため,たいていの JavaScript が元サイズより大きくなってしまう(笑)
    • ということで非実用/実験向き?
  • packer
    • 方式: 静的辞書+可読文字符号化
    • cssQuery の Dean Edwards 氏作
    • 圧縮が速い
    • 静的辞書の生成は空白や記号によって区切られたトークン単位
    • マルチバイトはトークンとみなされない
    • それゆえ圧縮性能はそれほど高くない
    • 独自記法を用いることにより,難読化が可能*1
  • Extended ACSII Javascript Packer
    • 方式: 静的辞書+ISO-8859-1 特殊文字符号化
    • Crunching (コメントや余白を省くこと)はしてくれない
    • 辞書生成等動作が目に見えるので教育向き
  • MemTronic's Cruncher-Compressor
    • 方式: 静的辞書+ISO-8859-1 特殊文字符号化
    • インタフェース(にたどり着くの)が難しい*2
    • 圧縮レベルを選ぶことが出来る
    • 圧縮は遅い
    • その代わり圧縮率はかなり高い
    • 2KB 未満のファイルは圧縮してくれない(涙)

ほとんどが静的辞書法なので,圧縮率はいかに最適な辞書を生成するかにかかっています。簡易トークン化によって軽さをかせぐ packer とがんばって探索することで圧縮率をかせぐ memtronic など,性格の違いがあります。

「ISO-8859-1 特殊文字符号化(勝手に名付けた)」というのは,端的に言うと西欧圏でほとんど使われない chr(128) 以降(厳密には違うのでつっこまないで下さい)を,辞書へのポインティングに使ったものです。ですから単純な展開ルーチンだとマルチバイトな環境でうまく展開できない可能性があります。memtronic の場合,一応配慮したコーディングをしてあるそうです。実際 UTF-8スクリプトではうまく動きました(辞書ポインティングの部分が結局複数バイトになってしまうのであまり圧縮のうまみがありませんが)。

なお,packer のたぐいは JavaScript 難読化の用途には使えません。デコーダの「eval」あるいは「document.write」の部分を alert なり,console.log(FireBug を使っている場合)に変えるとデコード結果が簡単に取得できてしまいます。たいてい 1 linerですが JavaScript tidy のたぐいで整形してやればわかりやすくなります。

packer でお勉強

さて静的辞書法について目で見てみるために,一番メジャーと思われる packer で遊んでみましょう。packer のサイトに飛び,「Fast Decode」というチェックボックスと「Special Characters」というチェックボックスをはずします。また,Encoding は Normal とします。

ここで,下の「Paste」欄に

hello hello hello world

と入力して「Pack」ボタンを押します。

結果が「Copy」欄に

eval(function(p,a,c,k,e,d){while(c--){
if(k[c]){p=p.replace(new RegExp('\\b'+c+'\\b','g'),k[c])}
}return p}
('0 0 0 1',2,2,'hello|world'.split('|')))

のように出力されます(適宜改行を挿入しています)。

この「hello|world」が静的な辞書部分,「0 0 0 1」が圧縮データです。一目瞭然。蛇足ながらデコーダをもう少しわかりやすくインデントしてみます。

function (p, a, c, k) { // not used: 'e', 'd'
    while (c --) {
        if (k[c])
            p = p.replace(new RegExp("\\b" + c + "\\b", "g"), k[c]);
    }
    return p;
} ("0 0 0 1", 2, 2, "hello|world".split("|"));

単純な仕組みであることがおわかりいただけると思います。

おっと,では「0」や「1」というトークンを表現したい場合はどうなるでしょうか。

hello 0 hello 1 hello 2 world

を変換すると

eval(function(p,a,c,k,e,d){while(c--){
if(k[c]){p=p.replace(new RegExp('\\b'+c+'\\b','g'),k[c])}
}return p}
('3 0 3 1 3 2 4',5,5,'|||hello|world'.split('|')))

のようになりました。使われている数字リテラルに該当する辞書語の部分が空になっています。うまく考えたものです。

packer がよくできているのはこれだけではありません。辞書のサイズが

  • 10個以下
  • 36個以下
  • 62個以下
  • 63個以上

の場合で,デコードルーチンが変わるのです。実際に確かめてみてください。

*1:変数名の命名規則により引数やローカル変数を無名化してくれます。詳しくはヘルプ参照

*2:ブロックをクリック→「MemTronic's Place」というリンクをクリック