🧊

Tailwind CSSは、ViteのMPAで最適化できない

ということに仕事で困らされて、最近それなりの時間を持っていかれた記念のメモ。

とりあえずのワークアラウンドは見出したけど、あとでどこかの誰かがもっといい感じにやってくれへんかな〜って。

やりたかったこと

  • ViteのMultiple-Page機能を使ってアプリを作る
  • 各Pageでは、そのページで使われてる最低限のJS・CSSのみを出力したい

つまり、

  • たくさんのUIがあるPageA
    • JSもCSSもそれなりのサイズになることが予想される
  • ちょっとしたテキストがあるだけのPageB
    • JSはおろかCSSのサイズもちょっとだけ

ということがしたかった。

ViteのMPA

あえて書くまでもないけど、`vite.config.js`でこういう指定をするだけ。

const { resolve } = require('path')
const { defineConfig } = require('vite')

module.exports = defineConfig({
  build: {
    rollupOptions: {
      input: {
        pageA: resolve(__dirname, 'pageA/index.html'),
        pageB: resolve(__dirname, 'pageB/index.html')
      }
    }
  }
})

https://vitejs.dev/guide/build.html#multi-page-app

こうすると、

  • `pageA/index.html`からなる一連の依存グラフによるページ
  • `pageB/index.html`からなる一連の依存グラフによるページ

それぞれが生成されるようになる。

あとは互いを単なる`a[href]`でつなげれば、SPAではないけど、あたかも1つのアプリのように見せることができる。

これがなかなかに気の利くやつで、例えばこのアプリをReactでやってた場合、それはAでもBでも使われる共通のチャンクに入れて最適化してくれたりする。
AにはいろんなUIがあってバンドルサイズが多少ふくらんでも、BにそれがないならBは小さいままになる。いい感じ。

最適化されないCSS

最適化される = 冒頭で書いたやりたいことが実現できるということ。

正確には、ViteがデフォルトでサポートしてるCSS Modulesの仕組みだと、JSと一緒でちゃんと最適化される。
CSS in JSの場合も、もちろんそれはもはやJSなので、最適化される。

記事タイトルにもあるように、Tailwindを使った場合だけが問題。

というのを検証したリポジトリがこちら!

GitHub - leader22/vite-mpa-tailwind-css-bundle-size

READMEに`vite build`のログを貼ってあるけど、AとBで同じサイズのCSSが出力されてしまってる・・。
Aではたくさんのユーティリティクラスを書いてて、Bではほとんど書いてないのにも関わらず。

原因

従来のCSSは、

  • 使うものを自分で定義
  • ソースにもそれをそのまま書く
  • そしてバンドルする

いわばボトムアップのアプローチで、書き手にすべてが委ねられてた。

Tailwindはその反対で、

  • デフォルトで莫大なカタログが用意されてる
  • 必要なものをソースに書く
  • 使われてるものだけをバンドルする

っていうトップダウンなアプローチになってる。

なので、Tailwindで最終的なバンドルサイズを削減するためには、使われてるものを明示して後は捨てるための`purge`の指定が必須。
さもないと、ものすごい数のユーティリティクラスがバンドルされてしまう。

で、この指定は`tailwind.config.js`で静的に対象ファイルのリストを定義する必要がある。`["./app1/**/*.jsx"]`みたいに。

そして、その`tailwind.config.js`が`postcss.config.js`から参照されて、それが`vite.config.js`で参照される。
ここに、「今どのページのためのCSSをバンドルしてるのか?」っていう実行コンテキストの概念がないのが原因。

ページAのためのCSSを処理してるなら、`purge`にはAで使ってるファイル群だけが列挙されてほしい。
しかし、設定ファイルがいわばグローバルなので、せめてもの抵抗として全ページを並べるしかない・・。

その結果できあがるのは、`全ページの最大公約数.css`であり、ぜんぜんUIのないページBにおいてもそれが使われてしまう・・というわけ。

そもそも、グローバルで後からふるい落とすっていうTailwindの考え方が、Viteやそれ系のLoader/Pluginの考え方にマッチしてないだけなのかなーとは思う。
実行コンテキストも何も、そんな概念にとらわれずに`.css`だけ処理すればいいはずちゃうんけ!って。私もそう思います。

ワークアラウンド

と言ってしまっていいのか微妙やけど、いちおう。

というのは、

  • `vite build`でViteにCSSをビルドしてもらう
  • その後で、自分でもCSSをビルドして、同名で上書きする

というもの。
開発中は無駄なサイズを気にしないことにして、ビルド時にだけつじつま合わせをする作戦。

NODE_ENV=production ./node_modules/.bin/tailwindcss \
  -i ./app2/style.css \
  -o ./dist/assets/app2.74ba676f.css \
  --minify \
  --purge ./app2/**/*.jsx

こういうコマンドのイメージ。
実際には上書きするファイル名は動的に変わるので、スクリプトでやるにはちゃんと書き換えが必要ではある。

ちなみにNextの場合も考えてみたけど、たぶん同様にページ単位で最適化することはできなくて、アプリ単位になっちゃうと予想。

あ〜誰かなんとかしてくれますように☆(別にTailwind推しでもなんでもない勢より)