これは何
今回、webpackのツリーシェイキングが意図せずうまくできていなかった部分を発見し、修正をしました。それにより、バンドルサイズを約30%削減することができました。 (Parsed Size 約890KB->約610KB)
今回、再exportをする際の注意点について学びがあったので、どのような問題が起きていたのかを説明します。
どのような実装になっていたか
私達のプロジェクトでは、Feature-Sliced Designという設計を用いています。Feature-Sliced Designではレイヤーを分けて依存方向を制限します。
Feature-Sliced Designについては他のメンバーが分かりやすい記事を書いているのでこちらを読んでみてください。
このFeature-Sliced Designには「Public API」というルールがあります。
これは各モジュールのエントリーポイントを制限することで、内部実装への依存を防ぎ、変更に強くする狙いがあります。
具体的には、以下のように公開する機能を再exportして、外部からはそのexportされた機能のみをエントリーポイントからimportするようにしています。
// 依存されるモジュール側
export { A } from './a'
export { B } from './b'
export { C } from './c'
export { D } from './d'
// 使用する側
export { D } from '@/shared/lib'
どんな問題があったのか
本当は色々と複雑なことが起きていたのですが、単純化して解説していきます
上記の例でsharedのモジュールを使用しているentryファイルからすると、「D」の機能しか使っていません。しかし、D以外のA〜Cも最終的なファイルにバンドルされてしまっていました。
Webpackでproductionビルドをする際に、ツリーシェイキングをしているので、使っていないものをある程度は除外してくれるのですが、今回はなぜかツリーシェイキングされていないことが分かりました。
補足: どのようにして原因を特定したのか
補足的に、どう調査をしたのかを記載しておきます。
今回、相対的にバンドルサイズが小さくなるはずのentryがあまり小さくなっていないことの原因調査を始めました。
すると、特定のライブラリ (上記でいうと「D」など)をimportしたときに急にバンドルサイズが大きくなることが分かりました。
調査方法は至って地味なのですが、以下のように特定のファイルのimportをコメントアウトして、webpack bundle analyzerで確認すると小さくなっていることが分かり、
// export { D } from '@/shared/lib'
次にimportしている部分を見て、以下の「D」以外をコメントアウトしたところ、サイズに変化がないことが分かり、D以外が影響していることが分かりました。
// export { A } from './a'
// export { B } from './b'
// export { C } from './c'
export { D } from './d'
どうしてツリーシェイキングが効かなかったのか
上記のファイルを特定してから具体的実装に踏み込んでみると、「C」の中で、「B」を使っていることが分かりました。
import { B } from '@/shared/lib'
このimportをする再、先ほどの再exportをしているファイルを経由していることがわかります。
つまり、以下のようにimportが循環していたのです。
- src/features/hoge/fuga.ts
- ↓
import { D } from '@/shared/lib'
- ↓
-
src/shared/lib/index.ts
- ↓
export { C } from './c'
- ↓
- src/shared/lib/c/index.ts
- ↓
export { C } from './c'
- ↓
- src/shared/lib/c/c.ts
- ↓
import { B } from '@/shared/lib'
- ↓
-
src/shared/lib/index.ts
- ↓
export { B } from './b'
- ↓
- ...
これにより、ツリーシェイキングがうまく効かなかったのだと考えられます。
他の場所でも同様に、再exportをしているファイルを複数回参照している部分がありました。
どう解決したのか
1. 再exportファイルを経由しないでimportするように変更
今回の直接の回避方法でいうと、index.ts
を経由しないように変更しました。
// 使用する側
- export { D } from '@/shared/lib'
+ export { D } from '@/shared/lib/d'
// 内部
- import { B } from '@/shared/lib'
+ import { B } from '../../b'
これで循環importを防ぎ、今後同様のことが起きないように src/shared/lib/index.ts
を削除しました。
2. 依存方向に例外的なことをしている部分の修正
Feature-Sliced Designでは、レイヤーごとに依存方向、同レイヤーへの依存の禁止が決まっています。
しかし、一部だけ例外的に依存方向のルールを破っている部分がありました。
このファイルをimportする再にも同様にツリーシェイキングができないことが分かったので、循環が起きないように変更対応を行いました。
副次的な効果
今回バンドルサイズを減らす効果の他に、ビルド時の使用メモリを削減する効果もありました。
今まで最終entryファイルにバンドルされていないもののビルド時に不要にimportされていたものも多く発生しており、過剰にメモリを使用していたことも分かりました。
まとめ
今回は再exportをする際の注意点についてバンドルサイズの観点で解説しました。
再exportをすることでバンドルサイズが大きくなる可能性があるので、依存方向には注意が必要です。
Feature-Sliced Designにはレイヤー間の依存関係に関してESlintの設定があるので、これを使うことで一定は防げます。しかし、例外的な処理をしている部分などは今回のことが起きないように対策を検討しています。