🎃

App Router時代のゼロランタイムCSS in JSに何を使えばいいの?

2023/09/12に公開

はじめに

こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!

https://www.oto-trip.com/

この記事では、Next.jsのReact Server Components(RSC) で使用可能なゼロランタイムCSS in JSライブラリを比較します。

目次

  • モチベーション
  • 使えるライブラリたち
    • 選定基準
    • 選定結果
  • 比較結果
    • 書き味
    • パフォーマンス
    • Dynamic Styling
  • 結論

モチベーション

みなさん、Next.jsのReact Server ComponentsのStyleをどうやるか問題に悩んでおられますね?

私もどれを使えばいいのかわからずNext.js公式に見に行くと、App Routerで使用できるものとして、以下のライブラリを上げています。

https://nextjs.org/docs/app/building-your-application/styling/css-in-js

が、React Server Componentsではnot supportedと記載されており、まだまだ未整備な状況のようです。

Warning: CSS-in-JS libraries which require runtime JavaScript are not currently supported in Server Components.

そこで、本記事で実際何を使えばいいんだろう?という素朴な疑問に自分なりの答えを出そうと思います。

使えるライブラリたち

選定基準

選定基準は以下の3点です。

  1. ゼロランタイムCSS in JSであること
  2. React Server Componentsで機能すること
  3. 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順)

https://github.com/kuma-ui/kuma-ui

https://github.com/callstack/linaria

https://github.com/chakra-ui/panda

https://github.com/vanilla-extract-css/vanilla-extract

比較結果

人気

まずはこれらのライブラリの人気度として、まずはインストール数を見てみます。

https://npmtrends.com/@kuma-ui/core-vs-@linaria/core-vs-@pandacss/dev-vs-@vanilla-extract/css

vanilla-extractlinariaの2強って感じですね。

次にState of CSS 2023の開発者満足度を見てみます。

https://2023.stateofcss.com/ja-JP/css-in-js/

こちらは、もはやvanilla-extractしか記載がないですね。

書き味

どのように記載するかをざっとみていきます。
簡単なCardっぽいUIを作ってみます。

kuma-ui
src/app/page.tsx
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
src/app/page.tsx
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
src/app/page.tsx
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
src/app/page.css.tsx
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)',
    },
  }),
};
src/app/page.tsx
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-uilinariaは、どちらかというとstyled-components風の書き方です。
一方でpanda cssvanilla-extractは、どちらかというとEmotion風の書き方です。

vanilla-extractは数字をpxと自動的に判断してくれますが、kuma-uilinariaは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
src/app/page.css.tsx
export const styles = {
  clicked: style({
    backgroundColor: 'red',
  }),
  unClicked: style({
    backgroundColor: 'blue',
  }),
};

src/app/page.tsx
const Component = ({ isClicked }: Props) => {
  return (
    <div className={isClicked ? styles.clicked : styles.unClicked}>
      Component
    </div>
  );
};

結論

個人的には一番Emotionっぽく使えるpanda cssに軍配が上がるかなーという感想です。
みなさんは、いかがでしたでしょうか。

こちらに記載された内容の詳細な情報は、以下のレポジトリを公開しております。
ライブラリの導入方法などよろしければ、そちらをご覧ください。

https://github.com/ototrip-lab/zero-runtime-css-in-js

最後に

ここまで読んでいただきありがとうございました。
Next.jsのApp Router関連技術はこれからもキャッチアップしていきたいです。

もし犬専用の音楽アプリに興味を持っていただけたら、ぜひダウンロードしてみてください!

https://www.oto-trip.com/

Discussion