目次Treeche とは?ExampleJavaScript の module バンドルにおいて副作用とは何なのかなぜ副作用で bundle-size が大きくなるのか?How to use?オプション動作原理は?Feedback

Treeche, Tree-Shakable Checker for JavaScript/TypeScript Application to reduce bundle size

2022/08/27
Programming
JavaScript
DX
Frontend

Treeche とは?

Treecheとは、Tree -Shakable Che ckerの略です. JavaScript/TypeScript module が Tree-Shakable かどうかを確認するためのツールです. バンドルサイズの削減やそれによるUserExperienceの最適化に有用です.

https://github.com/Shinyaigeek/treeche

Example

// this is not tree-shakable because have side-effect

const currentYear = new Date().getFullYear();

export function getCurrentYear() {
    return `Year ${currentYear}`
}

上記のような module は副作用を有しているため, tree-shakable ではありません。

このような場合、Treeche は以下のような出力をします.

🚨 ~/application/side_effect.ts is not tree-shakable due to the following code:


const currentYear = new Date().getFullYear();
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

下記のように, 副作用のある module を副作用のない module に修正して

export function getCurrentYear(currentDate: Date) {
    return `Year ${currentDate.getFullYear()}`
}

treeche を実行すると下記のような出力がなされます.

Congratulation 🎉 All files are tree-shakeable ✨

JavaScript の module バンドルにおいて副作用とは何なのか

ECMAScript のモジュールコンテキストにおける副作用とは, module の外部に影響を与えるコードのことを指します. 例えば、以下のようにモジュールのトップレベルで window オブジェクトに変数を設定するのは副作用です. module は import 時に実行されるため, module の top-level で window オブジェクトなどに mutation が加えられていると, module の import 時に実行される必要があります.

window.m = 1;
export mod() {
    return 1;
}

もちろん, この処理が init のような module から export される関数の中で実行された場合は, import 時にこれが実行されないので、副作用はなくなります。

なぜ副作用で bundle-size が大きくなるのか?

rollup の REPL で bundler の挙動を確認することができます。

URL: https://rollupjs.org/repl/?version=2.78.1&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMGhvZ2UlMjAlN0QlMjBmcm9tJTIwJTVDJTIyLiUyRm1vZCU1QyUyMiUzQiU1Q24lNUNuY29uc29sZS5sb2coaG9nZSgpKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTJDJTdCJTIybmFtZSUyMiUzQSUyMm1vZC5qcyUyMiUyQyUyMmNvZGUlMjIlM0ElMjJleHBvcnQlMjBmdW5jdGlvbiUyMGhvZ2UoKSUyMCU3QiU1Q24lNUN0cmV0dXJuJTIwMSUzQiU1Q24lN0QlNUNuJTVDbmV4cG9ydCUyMGZ1bmN0aW9uJTIwZnVnYSgpJTIwJTdCJTVDbiU1Q3RyZXR1cm4lMjBuZXclMjBEYXRlKCkuZ2V0RnVsbFllYXIoKSUzQiU1Q24lN0QlMjIlN0QlNUQlMkMlMjJvcHRpb25zJTIyJTNBJTdCJTIyZm9ybWF0JTIyJTNBJTIyZXMlMjIlMkMlMjJuYW1lJTIyJTNBJTIybXlCdW5kbGUlMjIlMkMlMjJhbWQlMjIlM0ElN0IlMjJpZCUyMiUzQSUyMiUyMiU3RCUyQyUyMmdsb2JhbHMlMjIlM0ElN0IlN0QlN0QlMkMlMjJleGFtcGxlJTIyJTNBbnVsbCU3RA==

main.js

import { hoge } from "./mod";

console.log(hoge())

mod.js

export function hoge() {
	return 1;
}

export function fuga() {
	return new Date().getFullYear();
}

上記のようなコードを rollup で bundle した結果は以下のようになります.

function hoge() {
	return 1;
}

console.log(hoge());

mod.js 内の fuga 関数が bundle の結果に存在しないのは, bundler が main.jsfuga 関数をインポートしていない(そしてもちろん使っていない)ことを知っているからで, そのため fuga 関数をバンドル出力に含める必要がなく, バンドルサイズの減少につながります。

しかし以下のように mod.js を編集するとどうなるでしょう.

export function hoge() {
	return 1;
}

const _fuga = new Date().getFullYear();

export function fuga() {
	return _fuga;
}

バンドルされた出力は、上記のようになります。

function hoge() {
	return 1;
}

new Date().getFullYear();

console.log(hoge());

https://rollupjs.org/repl/?version=2.78.1&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMGhvZ2UlMjAlN0QlMjBmcm9tJTIwJTVDJTIyLiUyRm1vZCU1QyUyMiUzQiU1Q24lNUNuY29uc29sZS5sb2coaG9nZSgpKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTJDJTdCJTIybmFtZSUyMiUzQSUyMm1vZC5qcyUyMiUyQyUyMmNvZGUlMjIlM0ElMjJleHBvcnQlMjBmdW5jdGlvbiUyMGhvZ2UoKSUyMCU3QiU1Q24lNUN0cmV0dXJuJTIwMSUzQiU1Q24lN0QlNUNuJTVDbmNvbnN0JTIwX2Z1Z2ElMjAlM0QlMjBuZXclMjBEYXRlKCkuZ2V0RnVsbFllYXIoKSUzQiU1Q24lNUNuZXhwb3J0JTIwZnVuY3Rpb24lMjBmdWdhKCklMjAlN0IlNUNuJTVDdHJldHVybiUyMF9mdWdhJTNCJTVDbiU3RCUyMiU3RCU1RCUyQyUyMm9wdGlvbnMlMjIlM0ElN0IlMjJmb3JtYXQlMjIlM0ElMjJlcyUyMiUyQyUyMm5hbWUlMjIlM0ElMjJteUJ1bmRsZSUyMiUyQyUyMmFtZCUyMiUzQSU3QiUyMmlkJTIyJTNBJTIyJTIyJTdEJTJDJTIyZ2xvYmFscyUyMiUzQSU3QiU3RCU3RCUyQyUyMmV4YW1wbGUlMjIlM0FudWxsJTdE

バンドルされている出力に不要な new Date().getFullYear() が含まれていることを確認できますね. なぜなら mod.jsnew Date().getFullYear() という副作用を孕むからです. JavaScript では、import されたモジュールは import 時に実行されるので、new Date().getFullYear() のような副作用のあるコードはそもそも実行されるべきであるため, bundler が削除することができません. しかしそもそも fuga 関数を import しないのであれば、new Date().getFullYear() がバンドルされた出力に含まれる必要はありません. なぜなら、もう一方の関数 hogeconst _fuga = new Date().getFullYear() を必要としないためです. なので理想としては new Date().getFullYear() というコードが出力からなくなっているべきです.

How to use?

Treecheをグローバルインストールして使うのみです.

npm install treeche -g
treeche "**/*.ts" --excludes "node_modules" "**/*.test.ts"

argument に入力のファイルを取ることができます. ここには Node glob pattern を利用することができます. また --excludes option で, 特定のファイル, フォルダを除外することができます. test ファイルや node_modules はノイズになるのでこれを用いて除外することが想定されています. また argument を省略した上で, --entry-point で単一のファイルを指定すると, そこを entrypoint として bundle した上で, その上で副作用があるかチェックしてくれます.

オプション

kindnamedescriptionexample
argumentinputsinput files to check tree-shakable. you can use Node glob patterntreeche "src/**/*.ts"
optionexcludesexcludes files to filter from inputs. you can use Node glob patterntreeche "src/**/*.ts" --e "node_modules"
optionentry pointthe unique entry point to check tree-shakable. if you specify input with this, treeche will bundle so you can check tree-shakable also in node_modulestreeche --entry-point ./src/main.ts

動作原理は?

動作原理はとても単純で, Treeche は JavaScript module bundler である rollup を内部で実行し、下記のように option で指定した入力モジュールを import している仮想エントリポイントコードをバンドルします。


import  "./your-module"

"./your-module" に副作用がある場合, import 以外のコードも出力されます. それをチェックしています.

Feedback

これは欲しいと思ってサクッと作った試験的なものですので, 多分バグがあります. 先に謝っておきます. バグや要望などあればフィードバックいただけると嬉しいです!

monkey-icon
earth
Hi 👋 I'm Shinobu Hayashi a.k.a Shinyaigeek(しにゃい).
Web Developer 🕸 / Reliable Web Enthusiast 🔥
Faster, Lighter, More accessible, More secure, More productive Web for anyone, anytime , anywhere.
twitter
github
linkedin
Copyright. 2022 Shinyaigeek