App Router時代のゼロランタイムCSS in JSに何を使えばいいの?
はじめに
こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!
この記事では、Next.jsのReact Server Components(RSC) で使用可能なゼロランタイムCSS in JSライブラリを比較します。
目次
- モチベーション
- 使えるライブラリたち
- 選定基準
- 選定結果
- 比較結果
- 書き味
- パフォーマンス
- Dynamic Styling
- 結論
モチベーション
みなさん、Next.jsのReact Server ComponentsのStyleをどうやるか問題に悩んでおられますね?
私もどれを使えばいいのかわからずNext.js公式に見に行くと、App Routerで使用できるものとして、以下のライブラリを上げています。
が、React Server Componentsではnot supported
と記載されており、まだまだ未整備な状況のようです。
Warning: CSS-in-JS libraries which require runtime JavaScript are not currently supported in Server Components.
そこで、本記事で実際何を使えばいいんだろう?という素朴な疑問に自分なりの答えを出そうと思います。
使えるライブラリたち
選定基準
選定基準は以下の3点です。
- ゼロランタイムCSS in JSであること
- React Server Componentsで機能すること
- CSS APIが使用可能であること(= Emotionっぽく使えること)
3つ目は、私が普段Emotionを以下のような使い方していてこの書き方から遠ざからない書き方になるように、と言う意味です。
import { css } from '@emotion/react';
const styles = {
dot: css({
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: 'gray',
}),
};
const Dot = () => {
return <div css={styles.dot} />;
};
UIコンポーネント系やtailwindcssのような書き方はほとんどしないので、今回は選定対象に入れておりません。
選定結果
ざっと動かしてみたところ、以下の4つのライブラリが候補になりそうです。
(ABC順)
比較結果
人気
まずはこれらのライブラリの人気度として、まずはインストール数を見てみます。
vanilla-extract
とlinaria
の2強って感じですね。
次にState of CSS 2023の開発者満足度を見てみます。
こちらは、もはやvanilla-extract
しか記載がないですね。
書き味
どのように記載するかをざっとみていきます。
簡単なCardっぽいUIを作ってみます。
kuma-ui
import { css } from '@kuma-ui/core';
const styles = {
imageWrapper: css`
padding: 12px;
`,
image: css`
width: 240px;
height: 240px;
border-radius: 8px;
`,
title: css`
font-size: 24px;
font-weight: bold;
`,
description: css`
font-size: 16px;
`,
card: css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s;
&:hover {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}
`,
};
const Card = () => {
return (
<div className={styles.card}>
<div className={styles.imageWrapper}>
<img src={src} className={styles.image} />
<div className={styles.title}>Title</div>
<div className={styles.description}>Description</div>
</div>
</div>
);
};
linaria
import { css } from '@linaria/core';
const styles = {
imageWrapper: css`
padding: 12px;
`,
image: css`
width: 240px;
height: 240px;
border-radius: 8px;
`,
title: css`
font-size: 24px;
font-weight: bold;
`,
description: css`
font-size: 16px;
`,
card: css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s;
&:hover {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}
`,
};
const Card = () => {
return (
<div className={styles.card}>
<div className={styles.imageWrapper}>
<img src={src} className={styles.image} />
<div className={styles.title}>Title</div>
<div className={styles.description}>Description</div>
</div>
</div>
);
};
panda css
import { css } from '../../styled-system/css';
const styles = {
main: css({
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: '12px',
}),
imageWrapper: css({
padding: '12px',
}),
image: css({
width: '240px',
height: '240px',
borderRadius: '8px',
}),
title: css({
fontSize: '24px',
fontWeight: 'bold',
}),
description: css({
fontSize: '16px',
}),
card: css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '16px',
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2)',
transition: '0.3s',
_hover: {
boxShadow: '0 8px 16px 0 rgba(0, 0, 0, 0.2)',
},
}),
};
const Card = () => {
return (
<div className={styles.card}>
<div className={styles.imageWrapper}>
<img src={'https://picsum.photos/240/240'} className={styles.image} />
<div className={styles.title}>Title</div>
<div className={styles.description}>Description</div>
</div>
</div>
);
};
vanilla-extract
import { style } from '@vanilla-extract/css';
export const styles = {
main: style({
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
}),
imageWrapper: style({
padding: 12,
}),
image: style({
width: 240,
height: 240,
borderRadius: 8,
}),
title: style({
fontSize: 24,
fontWeight: 'bold',
}),
description: style({
fontSize: 16,
}),
card: style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 16,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2)',
transition: '0.3s',
':hover': {
boxShadow: '0 8px 16px 0 rgba(0, 0, 0, 0.2)',
},
}),
};
import { styles } from './page.css';
const Card = () => {
return (
<div className={styles.card}>
<div className={styles.imageWrapper}>
<img src={src} className={styles.image} />
<div className={styles.title}>Title</div>
<div className={styles.description}>Description</div>
</div>
</div>
);
};
kuma-ui
とlinaria
は、どちらかというとstyled-components風の書き方です。
一方でpanda css
とvanilla-extract
は、どちらかというとEmotion風の書き方です。
vanilla-extract
は数字をpxと自動的に判断してくれますが、kuma-ui
とlinaria
はpxを記載する必要があります。panda css
の数字はremになるのでpx表記の場合は上記の通りちゃんと記載してあげる必要があります。
個性がありますね。
パフォーマンス
上記CardをAWS Amplifyにホスティングして、描画にかかる時間を計測してみます。
初期速度は、AWS側のCold Start時間に比例します。
再描画は、ブラウザキャッシュを削除した状態です。
いずれも、別の時間帯に3回計測した結果です。
対象 | 初期速度(秒) | 再描画(ミリ秒) |
---|---|---|
kuma-ui | 1.68 | 210 |
linaria | 1.62 | 216 |
panda css | 1.57 | 195 |
vanilla-extract | 1.63 | 239 |
想定通り、ほとんど優位な差がないですね。
Dynamic Styling
ゼロランタイムCSS in JSの弱点は、Dynamicなスタイル変化に弱いと言う点です。
トランスパイル時に解読できない場合、スタイルが付与できないためです。
panda css
の公式ページにこの問題に関する記載があるので転記すると、
// ❌ Avoid: Runtime value (without config.`staticCss`)
const Button = () => {
const [color, setColor] = useState('red.300')
return <styled.button color={color} />
}
みたいなことができないんですね。
正直、こういうことがやりたくてCSS in JS使ってるところがあるのにもどかしいですが、使えないものはしょうがないです。
では、各ライブラリでどのように実装すればいいんでしょうか。
基本的には各ライブラリ同じように、必要なスタイルをあらかじめ用意しておく戦略が必要です。
kuma-ui
type Props = {
isClicked: boolean;
};
const Component = ({ isClicked }: Props) => {
return (
<div
className={
isClicked
? css`
background-color: red;
`
: css`
background-color: blue;
`
}
>
Component
</div>
);
};
linaria
const Component = ({ isClicked }: Props) => {
return (
<div
className={
isClicked
? css`
background-color: red;
`
: css`
background-color: blue;
`
}
>
Component
</div>
);
};
panda css
const Component = ({ isClicked }: Props) => {
return (
<div
className={
isClicked
? css({ backgroundColor: 'red' })
: css({ backgroundColor: 'blue' })
}
>
Component
</div>
);
};
vanilla-extract
export const styles = {
clicked: style({
backgroundColor: 'red',
}),
unClicked: style({
backgroundColor: 'blue',
}),
};
const Component = ({ isClicked }: Props) => {
return (
<div className={isClicked ? styles.clicked : styles.unClicked}>
Component
</div>
);
};
結論
個人的には一番Emotionっぽく使えるpanda css
に軍配が上がるかなーという感想です。
みなさんは、いかがでしたでしょうか。
こちらに記載された内容の詳細な情報は、以下のレポジトリを公開しております。
ライブラリの導入方法などよろしければ、そちらをご覧ください。
最後に
ここまで読んでいただきありがとうございました。
Next.jsのApp Router関連技術はこれからもキャッチアップしていきたいです。
もし犬専用の音楽アプリに興味を持っていただけたら、ぜひダウンロードしてみてください!
Discussion