🔖

terser で複数回 minify することに意味があるパターン

2022/03/25に公開

一部のライブラリ作者が terser を複数掛けているのは知識として知っていたのですが、それのホットスポットを見つけたので記事にしておきます。

この記事で書いた内容はすべて https://try.terser.org/ のデフォルト設定で試したものです。

前提

terser がオブジェクトの定数メンバをどう展開してくれるかを調べようとしました。ネストされた「定数であってほしいもの」をどう解釈するかの実験です。

入力

const x = {
  FOO: 1,
  BAR: 2
};

console.log(x.FOO);

出力

console.log(2);

これが理想的なパターン。

ここで関数を混ぜてみます。

const x = {
  FOO: 1,
  BAR: 2,
  f() {}
};

console.log(x.FOO);

出力

const o={FOO:1,BAR:2,f(){}};console.log(o.FOO);

最適化が崩れてしまいました。

おそらく理由としては、 f() {}thisx を指すので、x に紐づく FOO|BAR も消えなくなってしまった、と想像できます。

なので、this を消すために Arrow Function にしてみます。

const x = {
  FOO: 1,
  BAR: 2,
  f: () => {}
};

console.log(x.FOO, x.f());

出力

const o=1,c=()=>{};console.log(o,c());

元の x に相当するオブジェクトが消え、FOO と f が別のものとして扱われているみたいです。

複数回掛けると縮んだパターン

ここから本番。これを深くしてみました。

const x = {
  FOO: 1,
  BAR: 2,
  f: ()=>{
  },
  NEST: {
     DEEP: {
      V: 1
     }
  }
};
console.log(x.NEST.DEEP.V, x.f());

出力

const o=()=>{},E={DEEP:{V:1}};console.log(E.DEEP.V,o());

x.NEST と x.foo が無関係なので、 NEST と foo で destrcturing (分解)が行われたように見えますね。

なるほどーと思ったんですが、これよく見たら定数置換ができるパターンそのものに見えます。なので、もう一回 terser を掛けてみました。

2回目

入力

const o=()=>{},E={DEEP:{V:1}};console.log(E.DEEP.V,o());

出力

console.log(1,void 0);

ほとんど消えました。

おそらくネストの分解を再帰的にやってなくて、オブジェクトに対して再帰ではなく、一層だけやってる、みたいな状態っぽく見えますね。

1回と複数回実行がもっとも大きくなるパターン

というわけで、試してみたのがこれ

// 134bytes
const x = {
  A: { B: { v: 2, C: { D: { v:4, E: { F: { v: 6 } } }}} }
};
console.log(
  x.A.B.v, // 2
  x.A.B.C.D.v, // 4
  x.A.B.C.D.E.F.v, // 6
);

偶数の深さに v を置いて、それにアクセスするパターンを作りました。

// 1回目
const v={B:{v:2,C:{D:{v:4,E:{F:{v:6}}}}}};console.log(v.B.v,v.B.C.D.v,v.B.C.D.E.F.v);

// 2回目
const v={v:2,C:{D:{v:4,E:{F:{v:6}}}}};console.log(v.v,v.C.D.v,v.C.D.E.F.v);

// 3回目
const o=2,v={D:{v:4,E:{F:{v:6}}}};console.log(o,v.D.v,v.D.E.F.v);

// 4回目
const o={v:4,E:{F:{v:6}}};console.log(2,o.v,o.E.F.v);

// 5回目
const o=4,c={F:{v:6}};console.log(2,o,c.F.v);

// 6回目
console.log(2,4,6);

というわけで1回だけだと 134 bytes => 85bytes でしたが、最終的に 19bytes になりました。

まとめ

ここから導かれるベストプラクティスは、とりあえず terser を複数回掛ける… ではなく、「定数宣言はオブジェクトメンバでやらない」ということです。terser にとっては無用な苦労です。今回、あえて偶数の深さに v を置いたのはそれが terser が機能するパターンだからで、奇数の場所に置くとそもそも minify が起きませんでした。これに人間が気をつけて書くのは無理です。

とはいえ、 terser を複数回掛けることに意味はあるかと言われたら、一部のケースでは確かにある、ということがわかります。よほどビルドサイズを絞りたい環境以外、実際には1回で十分だと思いますが。

Discussion