Emotion からの移行先として Panda CSS は妥当なのか

この記事は READYFOR Advent Calendar 2023 の11日目の記事です。

READYFORでプロダクトエンジニアをしている橋本です。

最近の取り組みの中で、CSSライブラリの変更の検討をしています。
いくつかのライブラリの検討を並行して行っていますが、そのなかでPandaCSSにフォーカスを当ててまとめます。


## Emotionの現状

EmotionはJavaScriptで書かれたCSS in JSライブラリであり、コンポーネントベースのスタイリングを容易にすることが特徴です。styled-componentsと並んで、現在でも活発に利用されていますが、利用する際には以下の問題点があります。

#### スタイルの生成がランタイムで行われる
Emotionで書かれたコードでは、すべてのスタイル文字列がJavaScriptにバンドルされ、ランタイム時にブラウザ上で実際に動作するスタイルタグへの変換が行われます。この挙動により、バンドルサイズの肥大化および実行時のパフォーマンスへの影響が懸念されます。

#### Server Componentの非対応
現状、Emotionはブラウザ側でのみ動作するため、サーバー側で生成されるServer Componentsとの併用ができない状況です。Server Componentsを活用しているアプリケーションでEmotionを利用する場合、実装方法に大きな制約が課され、コードの複雑さを増加させる原因となっています。


## Panda CSS について

> CSS-in-JS with build time generated styles, RSC compatible, multi-variant support, and best-in-class developer experience

Panda CSSのトップページによると、その大きな特徴の一つとして、ビルド時にスタイルを生成することが挙げられます。このアプローチにより、Emotionで指摘されたランタイムのパフォーマンスへの影響をほぼゼロに抑えることが可能です。また、生成されたスタイルはJavaScriptファイルとは別のCSSファイルとして出力されるため、バンドルサイズの削減やCDNを利用したキャッシュの最適化など、コンテンツ配信の効率化が期待できます。
さらに、Server Componentsとの互換性に関しても、実行時に動作するスタイルが存在しないため、問題はありません。


## 置き換えコストについて

ビルド時にスタイルを生成するタイプのcssライブラリに共通してある問題として、Dynamic stylingに関する制約があるため、Dynamic stylingがある場合と、ない場合とでそれぞれ置き換えを行う場合のコストについて考えます。

#### Dynamic styling がない場合

Emotion 及び Panda CSS はどちらも Objectスタイル・Templateスタイルのサポートを行なっているため、基本的にはスタイリングに使用しているhelper関数の置き換えのみで移行を行うことができます。

**emotion**
```tsx
import { css } from "@emotion/react";

const Component = () => (
  <div className={css`
    color: #ffffff;
  `} />
)
```

**Panda CSS**
```tsx
import { css } from "../styled-system/css";

const Component = () => (
  <div className={css`
    color: #ffffff;
  `} />
)
```

#### Dynamic styling がある場合

例えば以下コードはEmotionでは動作しますが、Panda CSS　では動作しません。

**emotion**
```tsx
import { useState } from "react";
import { css } from "@emotion/react";

const Component = () => {
  const [color, setColor] = useState("gray.50");

  return (
    <div className={css({ 
      color
    })} />
  )
}
```

**Panda CSS**
```tsx
import { useState } from "react";
import { css } from "../styled-system/css";

const Component = () => {
  const [color, setColor] = useState("gray.50");

  // このコードはエラーになります
  return (
    <div className={css({ 
      color
    })} />
  )
}
```

この問題の解決策として、Panda CSS では data-*属性の活用や、static css、tokenの利用などいくつかの方法が提供されています。ただ、いずれの方式を選択する場合でも、単純な移行作業では済まず、一定のコストをかけて以降を行う必要があります。

## monorepoとstorybookとの連携について

ここからはPanda CSS に置き換えて運用することについてフォーカスを当てます。


READYFORでは、UIコンポーネントを管理するための2つのライブラリをmonorepoで運用しています。これらのライブラリは2つのパッケージとして存在しますが、Storybookはルートディレクトリに1つだけ設置され、2つのパッケージを一元的に確認できるようになっています。

Panda CSSを使用するには、各パッケージに `panda.config.ts` を配置し、設定を行う必要があります。加えて、ルートディレクトリでStorybookを起動するためにも `panda.config.ts` が必要です。このため、実際に必要とされる数よりも多くの設定ファイルを管理するコストが増大しています。さらに、Panda CSSが生成するコード群（デフォルトでは `styled-system`）も各パッケージごとに生成されるため、これらをどのように管理するかが新たな課題となっています。

```
- .storybook
- packages/
  - core/
    - panda.config.ts
  - skin/
    - panda.config.ts
- panda.config.ts
```

## コンポーネントライブラリとしてどのように配信するか

Panda CSSでは、スタイリングを行うためのヘルパー関数群が準備スクリプトによって生成されます（デフォルトではstyled-system）。これらの関数群は実行時も必要なため、ライブラリとして配信するコンポーネントに利用する際は、このファイルの扱い方を検討する必要があります。

Panda CSSを使用したコンポーネントの配信方法については、公式ドキュメントでもいくつかの方法が紹介されています([参考](https://panda-css.com/docs/guides/component-library))。 現在検討中の方法としては、利用者側でPanda CSSに完全に依存しない形（利用者側でPanda CSSのセットアップが不要）を想定しており、styled-systemディレクトリとビルド済みのCSSを含める方向性を目指しています。

自社での移行検討の方針として、基本的にはこの方法で問題ないと考えていますが、CSSファイルをアプリケーション側でどのようにバンドルするかについて新たに検討する必要があります。例えば利用側がwebpackを利用している場合はcss-loader, style-loader等のセットアップを行った上でバンドルをするというコストが増えます。

## まとめ

結論として、Emotionからの移行先としてPanda　CSSを選ぶのは**かなりあり**だと思います。
monorepoでの扱いや配信方法の検討など、最初の段階で検討すべき難しい課題はありますが、そこを超えた先の実際の移行にかかるコストが小さく抑えられる点は、業務内での現実的な移行が見えるので十分に優位ではないでしょうか。

Dynamic stylingに関する課題はPanda CSSに限らず一般的なものです。この問題を克服すれば、従来の書き方とほぼ変わらないため、チームメンバーの間での浸透にも問題はないと思います。

ちなみに余談ですが、他の検討候補としてvanilla-extract、Tailwind CSSがあります。この３つの中の書き味含めた興味や好みとしても個人的にはPandaCSSに軍配があがります！

（アドカレ遅刻して申し訳ない...）