新しくnext.jsで作るプロダクトで CSS Modules か CSS in JS か決める調査や思考のログ
現状
社内の他のプロダクトは styled-components/astroturf を使っている
next.js * styled-componentsのプロダクトが既に1つある
今回、next.jsで新たなプロダクトを1つ作る
next.jsが組み込みでサポートしている最適は CSS Modules なのでこのままCSS in JSを使うか悩んでいる状態
また、CSS in JSにするとしても zero runtimeなものにするか悩んでいる状態
前提
- 実行速度は可能な限り上げたいものの、作ろうとしているのは実行速度を極力 上げる必要があるアプリケーションではない
- チームメンバーは今のところCSS in JSの知見しかない
- よほどメリットが上回らない限り、なるべくwebpackやbabelやpostcssなどの設定を書きたくない
感じていた課題
書きやすいが読みづらい
幾つかの記事で述べられているように、ただスタイルを付与して別名を付けたコンポーネントなのか、処理上/atomic design上の意味のあるReact Componentなのか判別しづらい
styled-componentsをnext.js SSRで動かすための対処
のように _doucment.tsx に一手間挟む必要がある
CSS in JSとstylelintの相性の悪さ
2020年秋頃だと babel設定が特定の条件を満たしているときに
https://github.com/stylelint/stylelint/issues/4732 のような問題が起きるので "stylelint": "13.3.0"
に固定する必要があった
また、幾つかのルールを無効化する必要があった
rules: {
'plugin/no-low-performance-animation-properties': true,
'value-keyword-case': null, // CSS in JSのcamelCase変数が壊れる
'function-name-case': null, // CSS in JSで使うfunctionが壊れる
'declaration-empty-line-before': null, // CSS in JSで宣言なくても怒る
},
みたいな回避をする必要があった
また、 const a = styled...
同士が空白行で空いていないとstylelintがかからないことがあるという問題も観測した
このようなCSS in JS * stylelint の怪しい挙動に付き合うのが嫌だったし、 問題はまだ山積みのように見える
sass-extract が deprecated なlib-sassに依存
開発事情としてSCSSがあり、SCSSに定義したデザイン用定数(breakpointや色やフォントサイズなど)をCSS in JSで使うために sass-extract を使っているのだが、これがnode-sass(libsass)に依存していて、 lib-sassはもうdeprecatedである
READMEにdart-sassを使えと書いてある
ちなみに next.js は node-sass と sass の両方が入っているとwarnを出す
その状態がちょっと嫌だった
2021/04/19 追記
stylelintのbabelとの相性問題は解決
CSS Modules の記事
CSS in JS の記事
styled-components
emotion
linaria
astroturf
styled-jsx
Cybozuの
styled-components を使う上で定めたのは次のようなルールです。
styledはコンポーネントを引数に取ってスタイルをあてる以外の使い方をしない
styledは同一ファイル内で使用してファイルを分割しない
は良さそうなんだけど、lintやformatterで強制できないルールを設けたくないなという気持ちは少しあるし、renderする構造が大きくなったらスタイルが書きづらくならないか気になる
その他 参考記事
パフォーマンスについて言及している記事には、「多くの場合はCSS-in-JSのコストは知覚できないが、コンポーネントツリーが巨大な場合は無視できなくなるかもしれない」というようなことが書いてあるので、ほとんどの場合は気にすることではない
CSS Modulesの利点
- CSSコーダーにとっては書き味がほぼ変わらない
- React Componentでないものはhtmlタグがそのままなので読みやすい
- パフォーマンスはCSS in JSよりいい
CSS Modulesの欠点・懸念
- 支配的な実装であるcss-loaderにおいてdeprecateの可能性がある
- forwardRefとかちゃんとしないと使いづらいシーンがありそう
- 複数class指定時、及び 別ファイルから複数composeしたときの順序保証しない問題 をちゃんと意識するのが難しい
- JSの値を使えないので、使いたいときにそこだけstyle propsで書くことになる。それはそれで統一感がない
- 非推奨 だし、CSSの全機能が使えたわけじゃなかった気がする
- next.js なら styled-jsx も最初から使える
- CSS in JSはやたらコンポーネント名考える必要があるよね?っていうけどCSS Modulesもclassname考える必要がある
astroturf * next.js 触った感想
-
css
で書くと(おそらく個別の)cssファイルにextractされる ようなことが書いてあり、そこがlinariaよりパフォーマンス上 優位に思える -
公式のexample を最新にしたりして試した
-
styles.chunk.*.css
みたいなファイルが1つできて、全ページでそれが<link rel="preload" href="/_next/static/css/styles.54eb5106.chunk.css" as="style">
のように読み込まれていた
-
-
Warning: Built-in CSS support is being disabled due to custom CSS configuration being detected.
が出る -
css
がstyled
でタグにスタイルつける書き方より書きやすいのか不明。React Componentと判別が難しい問題はなくなる - まだbetaなのが不安
- シンプルなツールなので自分でたくさんwebpack設定しなきゃいけない
2021/3/30 追記
以下のような感じに書けば普通に出来たし、next.jsの暗黙で持ってるpostcssなども効いたので特に設定が増えることもなかった
module.exports = {
webpack: (config) => {
config.module.rules.push({
test: /\.(tsx|ts|js|mjs|jsx)$/,
use: [
{
loader: 'astroturf/loader',
options: {
extension: '.module.css',
enableCssProp: true,
enableDynamicInterpolations: true
}
}
]
});
return config;
}
};
cssもページごとに分かれていた
astroturfがlinariaと違うのはcomposesやscssをサポートしている(というかライブラリ的には何もせずパスする思想なので使える)のでclassnameの嵐にならずに書けることである
最高に思えるが、これの唯一の懸念としては、(linaria * next.jsもそうだが)結局css-loaderのmodule: trueに依存してnext.jsのcss最適化に乗っけていることか
astroturfはpreprocessorを特に使ってないっぽいので、 && {}
みたいに優先度をアンパサンドで上げる ことはできないようだ
postcss-nestedを噛ませたらできた
astroturfほぼ完全に求めているものなんだけど、内部的にcomposesを使ってたりしてcss-modulesに完全依存しているのが、css-modules依存したくない前提だと本末転倒感あってウーン
linariaでnext-linaria 使って拡張子だけmodule.cssにしてnext.jsに食わせているのと依存レベルがちょっと違う
linaria * next.js 触った感想
-
https://zenn.dev/meijin/articles/a8163992c8e845fb382f#追記 には
全部同じ CSS ファイルに出力されるようです
とあるが、 この記事 に従って next-linaria を使って 公式のexample を若干書き換えてみると以下のようにcssがページごとに分割されることが確認できた- 共通で使っているComponentのcssは分離してくれず、それぞれに同じ内容が入っていた
- .linalia-cacheには *.linaria.module.css がページやコンポーネントごとに別々に生成されていた
.linalia-cacheにはvendor prefixが付いてるのに .next/static/cssの方にはついてない…なんでや- HTMLでは
<link rel="preload" href="/_next/static/css/bf12abf93e56e542a67f.css" as="style">
のように読み込めていた
├ ○ /fuga 418 B 67.3 kB
├ └ css/bf12abf93e56e542a67f.css 180 B
└ ○ /hoge 415 B 67.3 kB
└ css/edee91bf344d91b9abfd.css
- ページごとに分割される
大きなCSSになると思われる。styleがjsの中に入っているのとどちらがパフォーマンス上よいのか不明next.jsだとjsもpreloadで読まれているのでloadタイミング的な優位は特に無い。CSSOMツリーの構築とか、エンジンの解釈は早くなるかもしれない- CSS Modules使ったときの出力と同じだから多分next.jsにおいて最適なのだろう
.linalia-cacheにはvendor prefixが付いてるのに .next/static/cssの方にはついてない…なんでや
以下の要因で勘違いしており、 .browserslistrcに 'ie10', 'firefox 10', 'safari 8' みたいなのを入れたら vendor prefixがちゃんとついていることが確認できた
- next.jsデフォルトのbrowserslist設定は結構新しいので滅多にvendor prefixがつかない
- .linaria-cacheはlinariaの依存しているstylisでprefixつけたlinariaの出力であって、.next/static/css には使われないっぽいことを分かっていなかった
- そしてlinariaの出力はbrowserslistを見ずに雑にカバーするように大量のvendor prefixがついているぽい?
- .next/static/css はnext.jsがCSS in JSコードの方を直接postcss-preset-env経由でautoprefixerをかけていることを分かっていなかった
- 古いブラウザを指定するのを試す方法としてやってた、package.jsonのbrowserslist指定がなぜかpostcssに反映されない!
- .broserslistrcに書くとcss出力に反映されたが、気づくまで時間がかかってハマった
冒険せずにCSS Modulesと(おそらく)同じ最適化の出力が得られるなら、これが良い気がしている
あとは実装中に特有の不具合を踏まなければ…
動的な値について
-
styled
ならPropsが渡せる -
css
なら custom propertyが使える- Props方式に比べて型が弱いので
style={{ '--box-size': isSuccess ? '40px' : '10px' } as never}
のように潰す必要がある
- Props方式に比べて型が弱いので
生成されたCSSは両方custom propertiesで実装されていて、webpackが複雑に生成したjsの部分はよくわからないが、jsがそれを変えることでcssに反映されるっぽい
そういえば styled-jsx ってなんで使われてる例が目立たないのか
next.js組み込みのはずで、パフォーマンスも考えられているはずなのに、なぜか最近の記事であまり選定対象として議論されてなくて、存在感がないように感じる
CSS Modules * next.js 触った感想
- 設定なしでvendor prefixもついて望む最適化が得られるのは非常に楽
- ページごとに1枚に分割されていて、そこにそのページのためのスタイルが全部入っていて、preloadもちゃんとあるのはlinariaと同じ結果
- linariaもそうだが、共通 componentのchunkが分割される条件(サイズが肥大したときとか?)があるのかまでは不明
┌ ○ / 702 B 68 kB
├ └ css/f4803cd91d8adae24141.css 222 B
└ ○ /hoge 495 B 67.8 kB
└ css/9ec4e8d80e4624b73801.css 176 B
- clsxによる条件分けとVSCode上での typescript-plugin-css-modules による補完がうまく動いているので、書きにくくはない
大量のファイルがあり色んな所からcomposesすると順序不定問題にぶち当たるのは前職で苦しんだので、そこの不安を抱えたり、解決策を考えながら開発するのもなぁ CSS in JSの方が楽なんだよなぁという感じ
暫定結論
linariaにしてみる
理由
- (多分)CSS Modules使ったときの出力と同じなのでパフォーマンスがよさそう
- 駄目だったらstyled-componentsに戻しやすい
- あんまり設定が必要ない
- チームの既存技術スタック的にCSS Modulesを採用する冒険はやっぱり避けたい
その他
styled
にするか css
にするか
また、 styled
のときにコンポーネントにだけ使う縛りにするか
これはちょっと静的に縛りづらいので結論が出せない
(emotionだと書き方を設定できるのかな)
css
はライブラリによって微妙に渡し方違うので移行時の手間になりそうだし、メッチャクチャに困ってるわけじゃないので普通にstyledをいっぱい書く方針で良いんじゃないかと思えてきた
stylelintの課題も、まぁ…我慢できる
sass-extractはsassを共有変数定義のために使わない方針にすれば何とかなる
既に1プロダクトで使ってるastroturfが同じ出力ならlinariaよりsimple寄りということもあり採用したかったんだけど、 @zeit/next-css
起因なのかbuilt in CSSのwarnが出る上に1枚しか出力されないし、next-linariaみたいなのを自分で書いてメンテする気力と知識がまだないので見送る
linaria使ってて気づいた(他のライブラリも)の機能不足な点
styled-componentsやemotionでできる「 css
を css
や styled
にmixin,compositionする機能」がない
例: https://emotion.sh/docs/composition
また、以下のリンクを見ると
反対意見もありサポートされる気配がしばらくない
なので
のような隠蔽ができずclassnameでmixinするしかなさそう
linariaにはclsxみたいな cx
が用意されているのでそれでcss & classnameスタイルで書くことはできそう
css断片を css
を用いず文字列として定義して、それを ${}
で埋め込む方法でもうまく行った
静的なcssであればそれでもいいかもしれないが、ちょっと逸脱したハック感が強い
決定的な方法ではないが、優先度をアンパサンドで上げる 方法はそこそこ一般的なので、だいたい優先することが多い断片に適用するのはよさそう
&& {
font-size: 16px;
font-weight: 600;
line-height: calc(26 / 16);
}
styled と className と style の併用で優先スタイルを当てる手もあるが、なるべく避けて1つで統一したい
classnameが並んでるときの優先度がdeterministicでない問題はもともとcssにつきものだし、compositionで上書きしたいスタイルはそんなに多くないことを考えると、ノー対策でしばらく様子をみたい
決定的にするにはimport順ををeslintで定めるしかないよって言われたけど、それは既にやってるしナンカチガウ感
こんなライブラリも見つけた
css modulesほど思想に一貫性があって、かつ使われてしまったものが消えるとは思えないので、css loaderがサポートやめてもcss modules loaderみたいな感じに分離して生き残るんじゃないかと思ってはいる
でも思ったけど、next.jsでcss最適化に乗せたいと思うと、何のno runtime CSS in JSを使っても結局css-loaderのcss modules機能に依存することになるのが何とも将来の不確定要因になってしまっている
そこを絶対避けたいならstyled-componentsとかemotionとかを使うことになる
CSS Modulesになくてno runtime CSS in JSにある利点は、 CSS custom properties等を利用した動的な値を使うことが、(ライブラリが連携を隠蔽していることで)極めてしやすいことである
『styledつけただけのComponentなのかどうか判別しづらい問題』は namespace Styled
や const Styled = {}
で囲えば解決しそう
namespace使うと全部export constしなきゃいけないので後者が良さそう
zero runtimeのCSS in JSでcss-modulesに依存せずdeterministicにstyleが決まるcompositionがほしい件についての調査
前提を繰り返すが、classnameの羅列はdeterministicでないし、composesも別ファイルからだとdeterministicでない
scss のmixinは求めているものだけど、以下の理由でSCSS in JSに定義してもmixin困難
- astroturf: 同じファイル内だろうとcssブロックは全部自動で命名される別のファイルになるのでimportできない
- linaria: css出力はjsファイル単位だが、mixinが出力されない
postcss pluginもscssやcss-modulesよりずっとメジャーじゃないので使いたくない
@extend
で const css
の結果のclassnameを指定できないか?
妥協してscssの linaria
mixin使うときと同じで、next-linariaのここを変更して .module.scss
を吐くようにすれば使える
astroturf
const css
の中身はスペース区切りのclassnameが2つ取れるのでそのままだと無理
また、適当に文字列を分割して@extendしてもloaderの順番の問題でclassnameがまだ定まっていないのかscss errorが出て無理だった
どちらも、使えてもそうまでして使いたくない
結論
- styled-components使う
- やっぱりfragmentをdeterministicにcompositionできるのが強い
- 別にemotionでもいいが既存技術スタックと合わせる
所感
linaria, astroturf, (s)css modulesはどれも一長一短でかゆいところに手が届かなかった
linariaがcompositionに対応さえしてくれれば…
- linariaでも良いかもしれないケース
- non deterministicなclassname羅列方式でも許容できるなら
- astrturfでも良いかもしれないケース
- non deterministicなclassname羅列/composes方式や、内部的に完全にcss modulesに依存していることが許容できるなら
- css modulesでも良いかもしれないケース
- css modulesへの依存と、動的な値のつなぎにくさを許容できるなら
- 更にscss使っていいなら、composesの代わりにscss mixinを使えばdeterministicにできる
最近 next.js + MUI でstyled-components がうまく動かない状態 になっていて、docに
❗ Warning: Using
styled-components
as an engine at this moment is not working when used in a SSR projects. The reason is that thebabel-plugin-styled-components
is not picking up correctly the usages of thestyled()
utility inside the@mui
packages. For more details, take a look at this issue. We strongly recommend usingemotion
for SSR projects.
と書かれたりして不穏な感じになっている
MUIと合わせるならemotionが無難そう
styled-commponentsはESMサポートが不完全で一部ビルドツールに噛ませると通らないという話 もあるらしい
emotionが大丈夫なのかは未調査
stylelint/postcss-css-in-js がずっとバグめいていて、ついにライブラリごとに分割路線になりそうだが動きが遅いのも、CSS in JSのString Stylesの保守性面で不安要素がずっとつきまとっている