Skip to content

Instantly share code, notes, and snippets.

@shqld
Last active December 20, 2023 15:27
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shqld/d101cae50dd83ab7d3487cdb10b80f4d to your computer and use it in GitHub Desktop.
Save shqld/d101cae50dd83ab7d3487cdb10b80f4d to your computer and use it in GitHub Desktop.
個人的にTerser & Babelの好きなところ

だれ?

(credit: @tomo_e さん)

  • 新卒 2 年目
  • ウェブチーム
  • フロント、BFF、CDN

Introduction

今日話すこと

  • 普段あまり気にしない(かもしれない)ビルドツールたちが頑張ってやってくれてること
  • ぼくたちにも手伝えること

今日日のビルドツールたち

Transpiler Bundler Minifier
Babel Webpack Terser
tsc Rollup UglifyJS
(etc.) Parcel babel-minify

Minifier & Transpiler

  • どのバンドルツールを使ってもお世話になるツール
  • AST を見てソースコードを加工する

Minifier: Terser

https://github.com/terser/terser

  • Modern UglifyJS (+ES2015)
  • 最近の Webpack のデフォルト Minifier

https://www.npmtrends.com/babel-minify-vs-uglifyjs-webpack-plugin-vs-terser

2 種類の Minify

  • Mangling
  • Compression

Mangling

  • 変数名を短くする
  • e.g. var o,t,r,i,f
  • いわゆる難読化
  • minify と言えば、な処理

Mangling Options: mangle-props

  • Object のプロパティ名まで mangle する
  • 無意識にやると危険なのでデフォルトはfalsewebpack.mode: 'production'でもfalse

Compression

  • 圧縮という名前だが、やってることは最適化に近い
  • 静的に解決できることは何でもやる
  • 個人的には Terser のこういうところが好き
  • 詳しくはドキュメントへ https://github.com/terser/terser#compress-options

inline (= 3)

  • inline 展開してくれる
  • 意外と webpack のデフォルトは最適化レベル MAX
    • 1 -- returnするだけの簡単な関数
    • 2 --引数あり
    • 3 --引数も変数定義もあり
const toBeInlined = () => {
  console.log('To be inlined')
}
toBeInlined()

// const toBeInlined = () => {
//    console.log("To be inlined");
// };
// console.log("To be inlined");
  • 普通に webpack を使ってたら元の関数を消してくれる
  • ちなみに eval()がコードにあると、最適化は効かなくなる → 本当に誰も使ってないかが担保できない

pure_funcs (= [])

  • 指定した関数がピュアかどうかを Terser に教える
const toBeInlined = () => {
  console.log('To be inlined')
}
toBeInlined()

// console.log('To be inlined')

passes (= 1)

  • minify し直す回数
  • e.g. 一回 mangle した後に効果を発揮する compression オプションと組み合わせる

hoist_props

  • 一回 mangle した後に効果を発揮するオプション

    var o={p:1, q:2}; f(o.p, o.q); // f(1, 2);

Unsafe Compression

  • 細かいところまで最適化して圧縮できる
  • (個人的には「そこまでするんだ」と思うけど、そういう姿勢は好き)
  • 実際のインプットと同じ挙動を保証できない
    • i.e. 挙動が異なってしまう可能性がある
  • デフォルトは全て false

unsafe_math

  • 2 * x * 36 * x
  • 計算によっては浮動小数の精度が落ちる場合がある
  • 結構すごいことをするが、あくまで静的に解決できる(AST だけで判断できるもの)ものだけ

prepack 🤔

https://prepack.io/

Conditional Compression

結構みんな使ってるやつ

if (DEBUG) {
  console.debug(`debug: ${sth}`)
}
if (process.env.NODE_ENV !== 'production') {
  assert(sth === 'sth', `${sth} is not sth`)
}
  • 3 つの処理の組み合わせ

仕組み(?)

ざっくり

  1. terser の DefineOption(webpack.DefinePlugin, etc.)で静的な値が埋め込まれる
   if ('production' === 'production') {
  1. その条件が静的に決定できるなら condionalsで値に置き換わる
   if (1) {
   if (0) {

https://github.com/terser/terser/blob/20606816104270551c85e8beec087229aadc1c19/lib/compress/index.js#L4437

  1. dead_code elimination でブロックが抽出/削除される
// if (1) {
assert(sth === 'sth', `${sth} is not sth`)
// }
// if (0) {
// assert(sth === 'sth', `${sth} is not sth`)
// }

Terser まとめ

  • 予め決定できる値は define する
  • pure なコードは最適化を強く効かせられるので、気兼ねなく可読性のための冗長なコードをかける
  • eval()は避ける
  • シンプルな JS を書いていれば、アグレッシブな compress オプションも結構使える
  • 最適な minify を研究すると面白いかも

Caveats

  • オプション追加による圧縮率は、コードに大きく依存する
  • もちろんアプリケーションコードでこのような minify の最適化にコストとリスクを負う必要性は薄い

Transpiler: Babel

  • 言わずと知れたトランスパイラ
  • 機能的にはプラグインの集合体だが、 基本的には @babel/preset-env が本体

Differential Loading

  • (今更感があるけど...)
  • tl;dr: ブラウザのサポートレベルに応じて異なる JS をロードさせる
<script type="module" src="modern.mjs"></script>
<script nomodule src="legacy.js"></script>
  • バンドルサイズも減る
  • 余計なポリフィルも減る

仕組み

type=”module” https://caniuse.com/#feat=es6-module

  • 比較的モダンなブラウザ
    • type="module"に対応している →esm として読み込む
    • nomodule属性の付いている<script>は無視する(Edge は無視するが fetch もする) →読み込まない
  • IE11 などのレガシーなブラウザ
    • type="module"を解釈できないので結果的に無視 →読み込まない
    • nomodule属性を解釈できないので結果的に無視 →読み込む

結果的にモダン/レガシーブラウザ向けに JS のビルドを分けられる

e.g.

JavaScript

  • async/awaitを使って regenerator-runtimeが必要になる
  • {String|Array}.prototype の何かを使って core-jsが必要になる

ウェブ全般

  • IE11 でイベント(e.g. onload, oninput)の発火タイミングが違う

こういうのを防げる。 (個人的には一番分けたいのは CSS だけど…)

@babel/preset-env: targets: "esmodules"

https://babeljs.io/docs/en/babel-preset-env#targetsesmodules

ビルドタイムで解決!

  • Run-time, Build-time
  • (当然だが)ランタイムでの処理は出来るだけ避けたい
const isModern🤔 = `noModule` in document.createElement('script
')

こんな感じ

  • browser.config.js
new DefinePlugin({
  'BUILD_ENVS.BUILD_ENV': `'${
    process.env.NODE_ENV === 'development' ? 'development' : 'production'
  }'`,
  'BUILD_ENVS.BUILD_PLATFORM': `'browser'`,
  'BUILD_ENVS.BROWSER_TYPE': isLegacy ? `'legacy'` : `'modern'`,
})
  • server.config.js
new DefinePlugin({
  'BUILD_ENVS.BUILD_ENV': `'${
    process.env.NODE_ENV === 'development' ? 'development' : 'production'
  }'`,
  'BUILD_ENVS.BUILD_PLATFORM': `'server'`,
  'BUILD_ENVS.BROWSER_TYPE': 'false',
})
  • usage
declare var BUILD_ENVS: {
  BUILD_ENV: unknown
  BUILD_PLATFORM: unknown
  BROWSER_TYPE: unknown
}
if (BUILD_ENVS.BUILD_ENV === 'development') {
  console.log('DEVELOPMENT_ONLY')
} else {
  console.log('PRODUCTION_ONLY')
}

if (BUILD_ENVS.BUILD_PLATFORM === 'browser') {
  console.log('BROWSER_ONLY')

  if (BUILD_ENVS.BROWSER_TYPE === 'legacy') {
    console.log('LEGACY_ONLY')
  } else {
    console.log('MODERN_ONLY')
  }
} else {
  console.log('SERVER_ONLY')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment