🔥

【App Router時代のNext.jsスタイリング戦略】CSS Modules vs Tailwind vs Zero-Runtime

2024/07/29に公開

はじめに

こんにちは!
株式会社BLUEISH エンジニアの佐々木(@osasasasa22)です。

App Router時代になってから、スタイリング戦略に悩んでいませんか?

App Router環境下では、JS実行を必要とするRuntime CSS-in-JSがサポートされなくなり従来のスタイリング戦略を見直す必要に迫られています。

本記事では、App Router時代におけるスタイリング戦略として、CSS Modules、 Tailwind CSS、 Zero-Runtime CSS-in-JSの3つのスタイリングアプローチを比較検討し、それぞれのメリット・デメリットを説明していきます!

(RutimeやZero-Runtimeについては別記事内で詳しく解説しています。)
https://zenn.dev/osasasasa/articles/e8bc1a5caf139f

BuildからRuntime時までの流れの解説

本記事ではパフォーマンスについても説明していきます。
それにあたって、Server ComponentsとClient Componentsの流れの理解と、Build時とRuntime時の流れをしっかりと理解していないと追いつけないので、Mermaidでフローチャートの作成を行いました。

実際に、Buildから利用者がブラウザにアクセスするまでの流れを表現しております。
改めてこの流れを理解しておいてください。

https://mermaid.js.org/

App RouterとCSS Modules

CSS ModulesはNext.js 9.2以降から標準機能として提供されています。
.module.css拡張子を使用して一意のクラス名を自動的に作成し、CSSをローカルスコープにします。
そのため、異なるファイル間でのでクラス名の衝突を気にせず使用できる、というものです。

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

App Router環境でのCSS Modulesの使用方法

App Router環境でのCSS Modulesの使用方法です。

  1. スタイルを適応させたいコンポーネントと同じディレクトリに、拡張子が.module.cssのCSSファイルを作成します。このファイル内で、通常のCSSルールと同様にスタイルを定義します。
    (ここで定義したCSSクラス名に、一意の識別子が自動的に付与されることで、クラス名自体が一意なものになります。)
app/components/Button/styles.module.css
.button {
  background-color: blue;
  color: white;
}
  1. 対象のコンポーネントファイル内で、import文を使用してCSSファイルをインポートし、stylesオブジェクトからCSSクラス名を取得します。
app/components/Button/index.tsx
import styles from './product-list.module.css';

const Button = () => {
  return (
    <button className={styles.button}>
      CSS Modules
    </button>
  );
};

Server ComponentsとClient ComponentsでのCSS生成タイミングの違い

CSS ModulesのServer ComponentsとClient Componentsでの、CSSの生成タイミングですが、Tailwind CSSとZero-Runtime CSS-in-JSとは異なっていきます。

┃Server Components

Build時にCSSを生成します。
そのため、Runtime時(javascriptの実行時)のオーバーヘッドがなく、パフォーマンスに優れています。ただし、動的なスタイルの適用はできません。

┃Client Components

Runtime時にCSSを生成します。
そのため、動的なスタイルの適用が可能です。
しかし、Runtime時のオーバーヘッドが発生するため、パフォーマンスに影響を与える可能性があります。

パフォーマンスとコード分割の観点

CSS Moduleファイルは、本番環境において自動的に最適化されます。

※最適化については公式ドキュメントに詳しく記載があります。
https://nextjs.org/docs/app/building-your-application/optimizing

この過程で、すべてのCSS Moduleファイルは自動的に最小化され、コード分割が適用された複数の.cssファイルに分割されます。
これらの生成された.cssファイルは、各ページやコンポーネントに対応しており、それぞれの表示に必要不可欠なCSSのみを含んでいます。
つまり、ページの表示に必要最小限のCSSだけがロードされるよう最適化されているのです。

結果として、バンドルサイズが最小化され、ページの読み込み速度が向上するなど、全体的なパフォーマンスの改善につながる、ということになります。

In production, all CSS Module files will be automatically concatenated into many minified and code-split .css files. These .css files represent hot execution paths in your application, ensuring the minimal amount of CSS is loaded for your application to paint.

App RouterとTailwind

Tailwind CSSは、ユーティリティーファーストのCSSフレームワークです。

ユーティリティーファーストとは、CSSの設計手法の一つで、小さな単一目的のCSSクラスを多数用意し、それらを組み合わせてデザインを構築するというものです。

例えば、「テキストを大きくする」「パディングを追加する」「背景色を変更する」といった個別の機能を持つクラスを予め定義しておき、HTMLの要素に直接これらのクラスを適用することでスタイリングを行うことができます。

このアプローチの大きな特徴は、再利用性の高さと柔軟性です。
同じクラスを異なる要素に適用することで、一貫したデザインを維持しやすくなります。
また、必要に応じてカスタムクラスを追加することで、独自のデザインシステムを構築することも可能です。

ユーティリティーファーストは、CSSの肥大化を防ぎ、命名の悩みをなくす一方で、HTMLが冗長になりやすいという課題もあります。

https://tailwindcss.com/docs/utility-first

App Router環境でのTailwindの設定と使用方法

TaileindCSSの設定手順についてはNext.jsの公式ドキュメントをご覧ください。
https://nextjs.org/docs/app/building-your-application/styling/tailwind-css#usage-with-turbopack

公式ドキュメントに従い、インストールとグローバルスタイルの追加が完了した後、アプリケーション内でTailwindのユーティリティクラスを使用できるようになります。

使用方法のイメージです。
HTMLの<buttton>タグにフォントサイズ、左右と上下ののpadding、背景色の指定をしてみました。

export default function Button() {
  return 
    <button class="text-lg px-4 py-2 bg-blue-500>
      Tailwind CSS
    </button>
}

出力されるとこのような表示になります。

Just-in-Time (JIT) エンジンとApp Routerの相性

Tailwind CSSのバージョン3以降、Just-in-Time(JIT)エンジンがデフォルトで有効になっています。

JITエンジンは、Build時に必要なCSSユーティリティクラスのみを生成するので、不要なCSSが含まれず、バンドルサイズが小さくなります。
また、必要なクラスのみを生成するため、Build時間が短縮され、ページの読み込み速度も向上します。

また、Build時に必要なCSSのみを生成するため、App Routerのサーバーコンポーネントでも問題なくスタイルを適用できます。

サーバーサイドレンダリングとの統合

App Routerは、サーバーサイドレンダリングをサポートしています。 Tailwind CSSは、サーバーサイドレンダリング環境でも問題なく動作します。 サーバーサイドレンダリング時に、Tailwind CSSのユーティリティクラスが適切に適用され、HTMLが生成されます。

App RouterとZero-Runtime CSS-in-JS

App Router時代に注目されるZero-Runtime

App Routerの登場により、Zero-Runtime CSS-in-JSの重要性がますます高まっています。
Zero-Runtime CSS-in-JSは、JavaScriptの実行時にスタイルを生成するのではなく、Build時に静的なCSSファイルとして生成します。
これにより、クライアントサイドのパフォーマンスが向上し、効率的なスタイル管理が可能となります。

代表的なライブラリには、

  • vanilla-extract
  • Stitches
  • Panda CSS
    などがあります。

Server Componentsとの互換性

Zero-Runtime CSS-in-JSは、React Server Components(RSC)との互換性を持っています。
RSCは、サーバーサイドでレンダリングされるReactコンポーネントで、クライアントサイドのJavaScriptの負荷を軽減し、パフォーマンスを向上させます。
Zero-Runtime CSS-in-JSは、Build時にスタイルを生成するため、RSCと組み合わせることで、さらに高効率なスタイル管理が可能です。

App Router環境でのvanilla-extractの設定と使用方法

App Router環境でZero-Runtime CSS-in-JSライブラリを使用するための設定と使用方法を紹介します。ここでは、Zero-Runtime CSS-in-JSライブラリの1つである、vanilla-extractを例に説明します。

まず、プロジェクトにvanilla-extractをインストールします。

npm install @vanilla-extract/css

次に、vanilla-extractを使用するための設定ファイルを作成します。
next.config.jsに以下の設定を追加します。

javascript
const withVanillaExtract = require('@vanilla-extract/next-plugin').withVanillaExtract;

module.exports = withVanillaExtract({
  reactStrictMode: true,
});

以上で設定が完了しました。

続いて使用方法です。
vanilla-extractを使ってスタイルを定義し、コンポーネントに適用します。
例えば、以下のようにスタイルを定義したとします。

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

export const button = style({
  padding: '20px',
  backgroundColor: 'lightblue',
});

次に、定義したスタイルをコンポーネントに適用します。

app/components/Button.tsx
import * as styles from './styles.css';

export function Button() {
  return (
    <button className={button}>
        vanilla-extract CSSを使用したボタン
    </button>
  );
};

Build時最適化とApp Routerのパフォーマンス

Zero-Runtime CSSは、実行時にCSSを動的に生成および挿入しないアプローチです。すべてのスタイルはBuild時に生成されるため、実行時に追加のオーバーヘッドが発生しません。
また、スタイルはBuild時に生成されるため、実行時に予期しないスタイルの変更や副作用が発生するリスクが軽減されます。

開発体験とパフォーマンスの比較分析

App Router環境下では、Server Components/Client Componentsのいずれも使用できることが前提となり、パフォーマンスと開発効率のバランスを追求することが重要です。
そこで今回は、開発体験での比較表とパフォーマンスでの比較表を作成し、それぞれの利点を出しました。

┃開発体験

┃パフォーマンス

Next.js App Router スタイリングアプローチのスコアリング表

今回、比較選定をする上で順位に合わせてスコアリングを行い、可視化しました。

【総合順位の算出方法】

各基準での順位をポイント化し(1位=3点、2位=2点、3位=1点)、合計点を算出しました。同率の場合は平均点を与えています。

┃開発体験比較スコアリング

順位

スコアリング

┃パフォーマンス比較スコアリング

順位

スコアリング

総合順位

総合評価

1位 Zero-Runtime CSS-in-JS (66.5点)

  • 開発体験とパフォーマンスの両面で最高スコアを獲得
  • 型安全性、コンポーネント設計、未使用コードの削除で特に高評価
  • ビルド時間と動的スタイリングに若干の制限あり

2位 CSS Modules (63点)

  • 開発体験とパフォーマンスでバランスの取れた高評価
  • 学習コストの低さとポータビリティの高さが強み
  • 型安全性やデザインシステム統合でZero-Runtime CSS-in-JSに劣る

3位 Tailwind CSS (55.5点)

  • 動的スタイリング、コード量、再利用性で高評価
  • プロジェクト規模適性が高い
  • 型安全性と未使用コードの削除で他の2つに劣る

まとめ

App Router時代におけるNext.jsのスタイリング戦略は、開発効率とパフォーマンスのバランスを追求する上で重要な検討事項となっています。
本記事では、CSS Modules、Tailwind CSS、Zero-Runtime CSS-in-JSという3つの主要なアプローチを詳細に比較分析しました。

スコアリング結果から分かるとおり、それぞれのやり方の良いところと悪いところがあります。
実際のプロジェクトの細かい要求事項や、開発チームが得意なことによっては、最適な選択が変わるかもしれません。その為、さまざまな状況や要因を考慮して、最終的な判断を行うことが大切だと思います。

今回私のプロジェクトでは、型の安全性と開発効率、パフォーマンスの観点からZero-Runtime CSS-in-JS「vanilla-extract」を選定しました。

App RouterのReact Server Componentsは、常に進化を続けており、Next.jsの今後のバージョンでは、スタイリング関連の機能強化や、新しいアプローチが導入される可能性が大いにあります。

こうした状況の中では、プロジェクトの規模や、開発チームのスキルセット、既存のコードベース、要件、将来の拡張性など、さまざまな要因を総合的に考慮し、他のアプローチとの比較を行い常に最善の選択を追求する姿勢が大切になります。

最後までお読みいただきありがとうございました。

Discussion