🍣

styled-components後の選択:CSS-in-JSは本当に終わったのか?

に公開

はじめに

最近、偶然にもstyled-componentsがメンテナンスモードに移行したというニュースを目にしました。調べてみると、styled-componentsのメンテナーであるEvan Jacobs(quantizor)が公式に発表していたようです。[1]
最近はstyled-componentsがあまり使われなくなってきている雰囲気なので、いつかはこうなるだろうと思っていましたが、少し不思議な感じがしました。

styled-componentsがメンテナンスモードに移行した理由

公式発表によると、styled-componentsがメンテナンスモードに移行した主な理由は大きく3つあります。

第一に、ReactのAPI変更です。ReactチームがContext APIなどのコアAPIを事実上廃止(de facto deprecate)することを決定しました。特にReact Server Components(RSC)ではContext APIを使用できず、マイグレーションパスも提供されていません。styled-componentsは内部的にContext APIに大きく依存しているため、これを代替する方法がないことが大きな問題でした。

第二に、フロントエンドエコシステムの変化です。全体的なエコシステムがCSS-in-JSの概念から離れており、Tailwind CSSのようなユーティリティファーストアプローチが爆発的な人気を得ています。このトレンドの変化により、CSS-in-JSライブラリの立場が大幅に縮小されました。

第三に、メンテナーの状況変化です。2018年からコアメンテナーとして活動してきたEvan Jacobs(quantizor)が、もはや大規模な本番環境でstyled-componentsを使用しなくなりました。実際の製品で使用していない状況では、ライブラリの発展方向を設定し、メンテナンスすることが難しいという現実的な問題がありました。

発表文でEvan Jacobsは明確に「新しいプロジェクトでは、styled-componentsや他のほとんどのCSS-in-JSソリューションを採用することは推奨しない」と述べています。ただし、既存のユーザーのために時々バグ修正や小さな改善は継続して提供する予定であり、React Contextのような既存のAPIは削除しないと約束しています。

Runtime CSS-in-JSの動作原理と限界

styled-componentsとemotionのようなRuntime CSS-in-JSは次のように動作します:

// styled-componentsの内部動作
const Button = styled.button`
  color: ${props => props.primary ? 'blue' : 'gray'};
`;

// ランタイムで次のようなことが発生
// 1. JavaScriptがpropsを評価
// 2. CSS文字列生成
// 3. <style>タグをDOMに挿入
// 4. ユニークなクラス名生成と適用

この方式の問題点は明確です。コンポーネントがレンダリングされるたびにJavaScriptがスタイルを計算する必要があり、これはパフォーマンスの低下につながります。特にSSRでは、サーバーで生成したスタイルとクライアントで生成したスタイルを同期する複雑なプロセスが必要です。

CSS-in-JSは本当に死んだのか?

多くの開発者がstyled-componentsとemotionの衰退を見て「CSS-in-JSは終わった」と言っています。しかし、これは大きな誤解です。実際、これら2つのライブラリはランタイムでスタイルを生成する方式で、パフォーマンス上の限界が明確でした。JavaScriptバンドルサイズの増加、ランタイムオーバーヘッド、SSRの複雑性など、技術的に不足している部分が多くありました。

しかし、Zero-Runtime CSS-in-JSという新しいパラダイムが登場しました。これらはビルド時にスタイルを生成してランタイムオーバーヘッドを除去しながらも、CSS-in-JSの利点である型安全性とコンポーネントスコープスタイリングをそのまま維持しています。つまり、CSS-in-JSの概念自体が死んだのではなく、より進化した形で発展しているのです。

マイグレーション選択肢

1. Zero-Runtime CSS-in-JS

Zero-Runtime CSS-in-JSは、ビルド時にスタイルを生成してランタイムオーバーヘッドを完全に除去します。依然としてJavaScript/TypeScriptでスタイルを記述しますが、最終的な成果物は純粋なCSSファイルになります。

Panda CSSの動作原理

Panda CSSは静的解析とコード生成を通じてビルド時にすべてのスタイルを処理します:

// 1. 開発者が書くコード
import { css } from './styled-system/css'

const buttonStyle = css({
  bg: 'blue.500',
  color: 'white',
  px: '4',
  py: '2',
  borderRadius: 'md',
  _hover: {
    bg: 'blue.600'
  }
})

// 2. ビルド時に生成されるAtomic CSS
/* 
.bg_blue_500 { background-color: rgb(59, 130, 246); }
.text_white { color: white; }
.px_4 { padding-left: 1rem; padding-right: 1rem; }
.py_2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.rounded_md { border-radius: 0.375rem; }
.hover\:bg_blue_600:hover { background-color: rgb(37, 99, 235); }
*/

// 3. ランタイムでは組み合わされたクラス名のみを渡す
export const Button = ({ children }) => (
  <button className={buttonStyle}> {/* "bg_blue_500 text_white px_4 py_2 rounded_md hover:bg_blue_600" */}
    {children}
  </button>
)

Panda CSSの核心はAST(Abstract Syntax Tree)解析です。ビルドツールがコードを解析してcss()関数呼び出しを見つけ、そのオブジェクトを分析して実際のCSSファイルを生成します。このプロセスで以下の最適化が行われます:

  • Atomic CSS生成:各スタイルプロパティを個別のクラスに分離して再利用性を最大化
  • Dead Code Elimination:使用されていないスタイルの自動削除
  • 型安全性:TypeScriptによる自動補完と型チェック

Chakra UIを作ったSegun Adebayoが開発したPanda CSSは、現在最も注目されているZero-Runtime CSS-in-JSソリューションです。React Server Componentsと完全に互換性があり、様々なフレームワーク(React、Vue、Solid、Svelte)で使用できます。

vanilla-extractの動作原理

vanilla-extractはTypeScriptの型システムを最大限活用してCSSを生成します:

// styles.css.ts
import { style, createTheme } from '@vanilla-extract/css'

// 1. テーマ変数定義
export const [themeClass, vars] = createTheme({
  color: {
    primary: 'blue',
    secondary: 'darkblue'
  },
  space: {
    small: '8px',
    medium: '16px'
  }
})

// 2. スタイル定義
export const button = style({
  backgroundColor: vars.color.primary,
  color: 'white',
  padding: `${vars.space.small} ${vars.space.medium}`,
  borderRadius: '4px',
  ':hover': {
    backgroundColor: vars.color.secondary
  },
  
  // メディアクエリサポート
  '@media': {
    'screen and (max-width: 768px)': {
      padding: vars.space.small
    }
  }
})

// 3. コンポーネントで使用
import { button } from './styles.css'
// ランタイムではハッシュ化されたクラス名のみ: "styles_button_xm8b9a"

vanilla-extractの特徴:

  • Type-Safe Contract:CSS変数をTypeScript型として管理
  • Zero-Runtime:すべての処理がビルド時に完了
  • CSS Variables活用:動的テーマ変更可能
  • Co-location制限.css.tsファイルでのみスタイル定義可能(意図的な設計)

この制約は、ビルドツールがスタイルファイルを明確に識別して処理できるようにするためのものです。

Linariaの動作原理と限界

LinariaはBabelプラグインを通じてテンプレートリテラルを分析し、CSSを抽出します:

import { styled } from '@linaria/react'

// 1. 開発者が書くコード
const Button = styled.button`
  background-color: ${props => props.primary ? 'blue' : 'gray'}; // ❌ 不可能!
  color: white;
  padding: 8px 16px;
  
  &:hover {
    background-color: darkblue;
  }
`

// 2. 実際に可能なコード
const primaryColor = 'blue'; // ビルド時に決定される必要がある

const Button = styled.button`
  background-color: ${primaryColor}; // ✅ 可能
  color: white;
  padding: 8px 16px;
`

Linariaの制約事項:

  • 静的評価のみ可能:すべての値がビルド時に決定される必要がある
  • 動的スタイル不可:propsベースのスタイリングはCSS変数を通じてのみ制限的に可能
  • 副作用禁止:importされたモジュールに副作用があってはならない
  • 未使用スタイル含有の可能性:ビルド時の抽出が完璧でない場合、不要なCSSが含まれる可能性がある

実際、APIが似ているように見えても、これらの制約により大幅なコード修正が必要な場合が多いです。

2. Utility-First CSS

Tailwind CSSの動作原理

Tailwind CSSは事前定義されたユーティリティクラスを組み合わせる全く異なるアプローチを使用します:

// 1. 開発者が書くコード
export const Button = ({ variant, children }) => (
  <button className={`
    bg-blue-500 text-white px-4 py-2 rounded
    hover:bg-blue-600 
    focus:outline-none focus:ring-2 focus:ring-blue-500
    ${variant === 'large' ? 'text-lg py-3 px-6' : ''}
  `}>
    {children}
  </button>
)

// 2. JITエンジンが使用されたクラスのみを抽出
/* 
.bg-blue-500 { background-color: rgb(59 130 246); }
.text-white { color: rgb(255 255 255); }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.rounded { border-radius: 0.25rem; }
.hover\:bg-blue-600:hover { background-color: rgb(37 99 235); }
*/

Tailwind CSSの核心技術:

  1. JIT(Just-In-Time)コンパイラー

    • コードをスキャンして使用されたクラスのみを抽出
    • 任意の値もサポート:w-[137px]bg-[#1da1f2]
    • ビルド時間の大幅短縮
  2. PostCSSプラグインアーキテクチャ

    // postcss.config.js
    module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},
      }
    }
    
  3. v4の自動コンテンツ検出

    • 設定ファイルなしで自動的にすべてのファイルをスキャン
    • .gitignoreファイルの自動認識
    • よりスマートなクラス抽出

Tailwindの長所と短所:

  • 長所:一貫したデザインシステム、高速プロトタイピング、小さな最終CSSサイズ
  • 短所:HTMLの可読性低下、初期学習曲線、カスタムデザインの複雑さ

変換例

// Before: styled-components
const Button = styled.button`
  background-color: ${props => props.variant === 'primary' ? props.theme.colors.blue[500] : 'transparent'};
  padding: ${props => props.size === 'large' ? '12px 24px' : '8px 16px'};
`

// After: Panda CSS
const buttonRecipe = cva({
  base: {
    cursor: 'pointer',
    transition: 'all 0.2s'
  },
  variants: {
    variant: {
      primary: { bg: 'blue.500', color: 'white' },
      outline: { bg: 'transparent', border: '1px solid', borderColor: 'blue.500' }
    },
    size: {
      large: { px: '6', py: '3' },
      medium: { px: '4', py: '2' }
    }
  }
})

const Button = ({ variant, size, children, ...props }) => (
  <button className={buttonRecipe({ variant, size })} {...props}>
    {children}
  </button>
)

どの選択が最善か?

プロジェクトの状況によって最善の選択は異なります。

新しいプロジェクトを始める場合、Panda CSSまたはTailwind CSSをお勧めします。どちらも現代的でパフォーマンスに優れており、活発にメンテナンスされています。デザインシステムを構築する必要がある場合はPanda CSSが、迅速なプロトタイピングが目的の場合はTailwind CSSが有利です。

既存プロジェクトをマイグレーションする場合、プロジェクトの規模と複雑さを慎重に検討する必要があります。LinariaはAPIが類似しているように見えますが、実際には多くの制約があり、マイグレーションが困難な場合があります。むしろPanda CSSやvanilla-extractのように最初から異なるパラダイムを採用する方が良いかもしれません。

長期的な観点では、Zero-Runtime CSS-in-JSが主流になると思われます。Reactの方向性(Server Components、Streaming SSR)を考慮すると、ランタイムオーバーヘッドのないソリューションが必須だからです。

まとめ

styled-componentsのメンテナンスモードへの移行は一つの時代の終わりですが、同時に新しい始まりでもあります。CSS-in-JSは死んでいません。むしろZero-Runtimeというより良い形に進化しています。

技術スタックを選択する際は、単に流行に従うのではなく、プロジェクトの要件とチームの能力を考慮する必要があります。styled-componentsがまだ正常に動作している場合、すぐにマイグレーションする必要はありませんが、正直なところ、手遅れになる前にある程度の移行を検討すべきだと思います。


脚注
  1. styled-componentsメンテナンスモード移行公式発表 ↩︎

Discussion