(credit: @tomo_e さん)
- Sho Miyamoto / @webseals
- 新卒 2 年目
- ウェブチーム
- フロント、BFF、CDN
- 普段あまり気にしない(かもしれない)ビルドツールたちが頑張ってやってくれてること
- ぼくたちにも手伝えること
Transpiler | Bundler | Minifier |
---|---|---|
Babel | Webpack | Terser |
tsc | Rollup | UglifyJS |
(etc.) | Parcel | babel-minify |
- どのバンドルツールを使ってもお世話になるツール
- AST を見てソースコードを加工する
https://github.com/terser/terser
- Modern UglifyJS (+ES2015)
- 最近の Webpack のデフォルト Minifier
https://www.npmtrends.com/babel-minify-vs-uglifyjs-webpack-plugin-vs-terser
- Mangling
- Compression
- 変数名を短くする
- e.g.
var o,t,r,i,f
- いわゆる難読化
- minify と言えば、な処理
Mangling Options: mangle-props
- Object のプロパティ名まで mangle する
- 無意識にやると危険なのでデフォルトは
false
→webpack.mode: 'production'
でもfalse
- 圧縮という名前だが、やってることは最適化に近い
- 静的に解決できることは何でもやる
- 個人的には Terser のこういうところが好き
- 詳しくはドキュメントへ https://github.com/terser/terser#compress-options
- 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()
がコードにあると、最適化は効かなくなる → 本当に誰も使ってないかが担保できない
- 指定した関数がピュアかどうかを Terser に教える
const toBeInlined = () => {
console.log('To be inlined')
}
toBeInlined()
// console.log('To be inlined')
- minify し直す回数
- e.g. 一回 mangle した後に効果を発揮する compression オプションと組み合わせる
-
一回 mangle した後に効果を発揮するオプション
var o={p:1, q:2}; f(o.p, o.q); // f(1, 2);
- 細かいところまで最適化して圧縮できる
- (個人的には「そこまでするんだ」と思うけど、そういう姿勢は好き)
- 実際のインプットと同じ挙動を保証できない
- i.e. 挙動が異なってしまう可能性がある
- デフォルトは全て
false
2 * x * 3
→6 * x
- 計算によっては浮動小数の精度が落ちる場合がある
- 結構すごいことをするが、あくまで静的に解決できる(AST だけで判断できるもの)ものだけ
prepack 🤔
- 2019/1 月の PR 以降、特に動きがなさそう…
- 実際に運用してみた人がいたら教えてください 🙇♂️
結構みんな使ってるやつ
if (DEBUG) {
console.debug(`debug: ${sth}`)
}
if (process.env.NODE_ENV !== 'production') {
assert(sth === 'sth', `${sth} is not sth`)
}
- 3 つの処理の組み合わせ
ざっくり
- terser の DefineOption(
webpack.DefinePlugin
, etc.)で静的な値が埋め込まれる
if ('production' === 'production') {
- その条件が静的に決定できるなら
condionals
で値に置き換わる
if (1) {
if (0) {
dead_code
elimination でブロックが抽出/削除される
// if (1) {
assert(sth === 'sth', `${sth} is not sth`)
// }
// if (0) {
// assert(sth === 'sth', `${sth} is not sth`)
// }
- 予め決定できる値は define する
- pure なコードは最適化を強く効かせられるので、気兼ねなく可読性のための冗長なコードをかける
eval()
は避ける- シンプルな JS を書いていれば、アグレッシブな compress オプションも結構使える
- 最適な minify を研究すると面白いかも
- オプション追加による圧縮率は、コードに大きく依存する
- もちろんアプリケーションコードでこのような minify の最適化にコストとリスクを負う必要性は薄い
- 言わずと知れたトランスパイラ
- 機能的にはプラグインの集合体だが、
基本的には
@babel/preset-env
が本体
- (今更感があるけど...)
tl;dr
: ブラウザのサポートレベルに応じて異なる JS をロードさせる
<script type="module" src="modern.mjs"></script>
<script nomodule src="legacy.js"></script>
- バンドルサイズも減る
- 余計なポリフィルも減る
- 比較的モダンなブラウザ
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 だけど…)
- 2018/1 にサポート babel/babel#7212
- 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')
}