Open12

Next.js が CSS Modules を推奨する真相に迫りたい

Next.js 9.2 から CSS Modules がビルトインサポート対象になった。

https://nextjs.org/blog/next-9-2

CSS最適化に関して、組み込みサポートのレールから逸れると、ページ単位で最適化された静的CSSの生成不全に陥る。少しでもパフォーマンスの良い Next.js App を構築したいのなら、CSS Modules 一択というのが現状で、CSS in JS に慣れ親しんだ身からすると正直辛い現実だ。

CSS in JS よりも CSS Modules の方が、ロードタイム・ランタイムともに、パフォーマンス面で有利なことは知られている。こちらのベンチマークテストが参考になる。

http://necolas.github.io/react-native-web/benchmarks/

しかし、本来であるなら皆 CSS in JS を使いたいのではないか。あえてレガシーな CSS Modules 推奨とする理由は、CSS という難儀な仕組みを乗り越えるため、技術的に「そうせざるを得ない」理由が他にもあるのではないか。真相に迫りたい。

仮説1: 冪等性の担保

CSS定義を文字列として抽出し、コンポーネントが必要になったタイミングでCSSファイルを読み込めば良い、という単純な方法ではCSSはあっという間に崩壊する。以下のクイズの答えは「判定不能」だ。

詳細度が同一の場合、style の読み込み順で適用される style は確定する。よって定義読み込み順、ランタイムの振る舞いに応じて style が確定し、冪等性を担保する事できない。CSS Modules を採用していることにより、この冪等性担保に有利な何かが働いているのではないか。

これはすごくありそう。CSS Modulesは「読み込まれる順番は分からないから、composesを使ってスタイルごとにクラスをつくる(=複数のクラスをあてて上書きすることはしない)」ので、CSS Modulesが使われてる時点で冪等性担保になる。

Next.jsメンテナのTimerさんのコメントからもなんとなくそんな意図が読み取れるような…

that is not possible, nor a use case we're currently interested in exploring. CSS will be loaded the order it's imported by the application.

CSS Modulesのユースケースについてのコメント

CSS Modules: ... This prevents most nondeterminism issues, but doesn't completely eliminate them (best-effort). Its main purpose is to prevent a child-component from accidentally applying styles to a global selector like div (affecting the entire application).

CSS-in-JSにどっぷりではなかったので、違和感を感じていませんでした。

チュートリアルでは、次のように締めくくってますが、CSS-in-JSとの比較ではなさそうですね。

https://nextjs.org/learn/basics/assets-metadata-css/polishing-layout

Uesugiさんなら明確な答え持ってそうですね。

本来であるなら皆 CSS in JS を使いたいのではないか。

個人的には、CSS Modulesの方が好きなので、CSS Modules派も無視できないぐらい多かったのでは?と単純に思ったりします。スコープ維持しつつ、ファイル分離できて、CSS拡張子に記述できるという恩恵大きくて、悪くないと思っています。

SSR対応の側面はどうなんでしょう。styled-componentsだとSSR対応するために_document.tsxなどで各コンポーネントからCSSをかき集めるような苦しい処理を書くことになりますが(公式のexample)、CSS Modulesだとそのあたりの難しさがなさそうです(該当箇所はこのあたりかな)。

Linariaみたいなzero runtimeのCSS in JSではstyled-componentsのような問題は発生しないですが。

直接的なお話ではないですがこちらは参考になると思います。

https://github.com/vercel/next.js/discussions/15208#discussioncomment-38529

devongovettさんはParcelAdobe React Spectrumのメンテナで、React SpectrumNext.jsのIntegrationを対応させる際に苦労した経験などから意見を表明なさっています。

https://react-spectrum.adobe.com/react-spectrum/ssr.html#nextjs

事実、今のReact Spectrum + Next.js は辛そうです。
yarn add next-compose-plugins @zeit/next-css next-transpile-modules
と少々Hackyな解決方になっています。

CSS in JS よりも CSS Modules の方が、ロードタイム・ランタイムともに、パフォーマンス面で有利なことは知られている。

個人的にはこれが答えな気がしています!

本来であるなら皆 CSS in JS を使いたいのではないか。

私もこれまでずっとCSS in JS (SC, Emotion)を使っていたんですが、最近Next.jsビルトインのCSS Modules + Tailwindを使って考えが変わりました。スタイリングはclassNameに一任した方がスッキリするなーと。好みの問題だと思いますが。

あとはデザイナーさんとの協業だとCSS Modulesに軍配が上がりそうですね。経験則ですがデザイナーさんはSass派閥が多い印象です。

余談ですが、TailwindはNext.jsチュートリアルやSWR、Next.js Commerceなどのサイトでも使われておりVercel内部で流行っているように思います。

デファクトになり得るゼロランタイムCSS in JSライブラリか、Facebookのstylexのようなライブラリが出てきたら状況は変わるかも知れませんね〜

MiniCssExtractPlugin のオプション ignoreOrder: true としており、懸念していた読み込み順にまつわる Warning が出ない様に設定されていた。

https://github.com/vercel/next.js/blob/e5f9fbbcdf07e470b719781d81313271efd6711e/packages/next/build/webpack/config/blocks/css/index.ts#L322-L333

加てここのコメントにある通り、

  1. _app で読み込んだものは Global となる
  2. CSS Modules なら scoped な class名が付与される

と記されており、この制約で Next.jsはCSSの順序が「重要ではない」ことを保証するとしている。

仮説2: サードパーティを重視した副産物

Built-In CSS Support でも言及されている様に、サードパーティ受入への配慮が伺える。サードパーティもコンポーネント単位でスコープを閉じる ことが本来目的であり、App コードもこの仕組みを使えば CSS-in-JS をわざわざ入れなくても良い。静的CSSも得られるからお勧めだよ、ということか。

  • 仮説1は誤り
  • 仮説2はモチベーションとしてありそう
    • サードパーティのcssも、ローカルスコープに閉じたかったが、現状はそうなっていない
    • サードパーティ css チャンクに有利に働く最適化は、個人的にも欲しい
    • だが、上記の記事で触れている様に、複数コンポーネントで .module.css を読み込むことは悲劇をよぶ可能が高い

仮説3 Tailwind CSS 公式サポートの伏線

Tailwind CSS の公式サポートがRFCとして公開された。採用実績やニーズの高まりから、対応してしかるべきと。CSS が抱えていた課題を解決しながらも、最小限定義を静的ファイルとして抽出できる。

ユーティリティだけで賄いきれない擬似要素などの指定には、CSS Modules で賄うことができる。

https://github.com/vercel/next.js/discussions/20030
ログインするとコメントできます