🏃

App Router時代のZero-Runtimeの理解を深める

2024/07/27に公開

はじめに

従来のPages Routerに代わるルーティングシステムApp Routerの登場は、Next.jsを用いた開発を一変させました。
パフォーマンス向上や開発効率の改善をもたらしたApp Routerですが、同時にCSS-in-JSライブラリの扱い方にも大きな影響を与えています。

本記事では、App Routerの基本的な概念を紹介するとともに、App Router環境下でのCSS-in-JSの変化についてみていきます。
Runtime CSS-in-JS、Zero-Runtime CSS-in-JS、そしてHybrid CSS-in-JSといった異なるアプローチの特徴、利点、課題を比較することで、Zero-Runtimeについての理解を深め、App Router時代のCSS戦略をより明確に理解することを目指します。

App Routerとは

App Routerは、Next.js13.4で安定板が導入されたルーティングシステムです。
Server Components、Server ActionsなどのReactの最新機能を活用しており、より効率的でパフォーマンスの高いアプリケーション構築が可能になりました。

App Routerのルーティングについて

App Routerでは、ディレクトリ構造がそのままURLの構造になるため、直感的なルーティング設定が可能です。
以下に、従来のPages RouterとApp Routerの比較を示します。

Pages Routerの場合

 pages
      ├── signin/
      │    └── index.js
      └── _app.js       

App Routerの場合
例えば、以下の構成であれば、「/」にアクセスするとapp/page.jsが表示され、「/products」にアクセスするとapp/products/page.jsが表示されるようになります。

app
 └──  (Auth)           --> ルートグループ。URLには影響しないが、関連するページをグループ化する
     ├── layout.js     --> 共通化したいレイアウトを定義する
     └──  signin
          └── page.js  -->  /signinにアクセスした際に表示する内容を定義する

また、App Routerにおけるファイル規約はこのようになっています。

  • default.js:
    デフォルトの画面
  • error.js:
    404エラー画面
  • layout.js:
    共通のUIを設定する
  • loading.js:
    ローディング画面
  • middleware.js:
    リクエストが完了する前に実行されるコードを定義
  • not-found.js:
    notFound関数がスローされたときに表示する画面
  • page.js:
    ルートの画面
  • route.js:
    APIエンドポイントを定義
  • template.js:
    共通のUI。layout.jsとは異なり、状態を保持せず、毎回再レンダリングされる

https://nextjs.org/docs/app/api-reference/file-conventions

App RouterでのReact Server Components(RSC)の解説

Next.jsのApp Routerは、ReactのServer Components(RSC)の機能がサポートされています。
"use client"ディレクティブを使用して、Server ComponentとClient Componentを柔軟に切り替えられるため、アプリケーションの各部分に最適な実行環境を選択できるようになりました。
これにより、ブラウザにダウンロードするJavaScriptを最小限に抑えることができ、従来よりもパフォーマンスが高く、画面の肥大化によるパフォーマンスの劣化が起きにくくなります。

Client Componentsのおさらい

Next.js 12で追加された、Client Componentsについて解説していきます。

Client Componentsとは

Client Componentsとは、JavaScriptの実行、HTML、CSSの組み立てがクライアントサイド(ユーザ側)で行われるコンポーネントを指します。
Client Componentsでは、以下のような流れでアプリケーションが動作します。

  1. ユーザがサーバーに対してリクエストを送信する
  2. サーバから圧縮されたHTML、CSS、JavaScriptが返される
  3. ユーザのブラウザ上で基本的なページ構造のHTML、CSSが組み立てられる
  4. JavaScriptが実行され、必要なClient Componentsが初期化される
  5. 非同期処理によりAPIサーバーからデータが取得され、対応するClient Componentsが更新されて、動的なコンテンツが表示される

Client Componentsのメリット

  • インタラクティブ性
    クライアントコンポーネントはブラウザ上でJavaScriptを使用して動的にコンテンツを生成・更新するため、ユーザーの操作に即座に反応できます。ページ全体をリロードせずに部分的な更新が可能となり、よりスムーズで直感的なユーザー体験を提供します。

  • リアルタイムデータの表示
    非同期通信を利用してAPIからデータを取得し、リアルタイムでコンテンツを更新することができます。チャットアプリや通知システムなど、リアルタイム性が重要な機能を効果的に実装できます。

  • 開発の効率化と再利用性
    各コンポーネントが独立しており、モジュール化されているため、再利用性が高くなります。これにより、コードの重複を減らし、開発やメンテナンスが容易になります。また、チームでの分業もスムーズに行えます。

Server Componentsのおさらい

Next.js 12で追加された、Server Componentsについて解説していきます。

Server Componentsとは

Server Componentsとは、JavaScriptの実行、HTML、CSSの組み立てがサーバ上で行われるコンポーネントを指します。これにより、ユーザ環境に依存しないサイトパフォーマンスの最適化が可能になります。
Server Componentでは、以下のような流れでアプリケーションが動作します。

  1. ユーザーがサーバーに対してリクエストを送信する
  2. サーバー上でServer Componentsが実行され、必要なデータを取得し、対応するServer ComponentsのHTMLが生成される
  3. サーバーから圧縮されたHTML、CSS、JavaScriptが返される
  4. ユーザのブラウザ上で基本的なページ構造のHTML、 CSSとServer Componentsで完全なページ構造 が組み立てられる
  5. JavaScriptが実行される。ただし、Server Componentsでは、追加のデータ取得や動的な更新は行われない
    ※Client Componentsを含むページの場合、5.の処理でClient Componentsの初期化が行われる

Server Componentsのメリット

クライアント側の負担軽減:
サーバーコンポーネントを使用すると、クライアント側で送信および実行されるコードの量を削減できます。クライアントモジュールのみがバンドルされ、クライアントで評価されます。

  • パフォーマンスの向上:
    サーバー上でコンポーネントが実行されるため、ローカルファイルシステムへのアクセスが可能で、データ取得やネットワーク要求の待ち時間が短縮される可能性があります。

サーバーコンポーネントの制限

  • 対話性の制限
    イベントハンドラーはクライアントによって登録およびトリガーされる必要があるため、サーバーコンポーネントはユーザーとの対話をサポートできません。例えば、onClickのようなイベントハンドラーはクライアントコンポーネントでのみ定義できます。

  • フックの制限
    サーバーコンポーネントでは、ほとんどのReactフックを使用することができません。

  • 状態の保持ができない
    サーバーコンポーネントがレンダリングされた後、その出力は基本的にクライアント側でレンダリングされるコンポーネントのリストになります。サーバーコンポーネントはレンダリング後にメモリに保持されず、独自の状態を持つことはできません。

https://react.dev/reference/rsc/use-client#advantages

何故、App RouterにZero-Runtimeが必要なのか

App RouterではZero-Runtime CSS-in-JSが推奨されています。
その理由について解説していきます。

背景

App RouterはRSC(React Server Components)などの機能がデフォルトで使用できるようになり、キャッシュを含めパフォーマンスを意識しています。
Runtime CSS-in-JSでとても人気なのはEmotionです。
EmotionやRuntime CSS-in-JS系のCSSフレームワークはスピード/保守性/可読性いずれも評価が高く、とても人気ではありましたが、大規模なアプリケーションであるほどオーバーヘッドが肥大化していきパフォーマンス面で強く影響があります。

何故、Zero-Runtimeが必要なのか

App RouterではRSCをベースにしています。
RSCはDOM などのブラウザ専用APIは使用できない為、ドキュメントにstyleタグを挿入しているRunttime CSS-in-JSは動作しません。
RSCでの観点でも、オーバーヘッドによるパフォーマンス観点でもZero-Runtimeが必要となっています。

https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#capabilities--constraints-of-server-and-client-components

CSS-in-JSとは

Runtime CSS-in-JSとZero-Runtime CSS-in-JSの解説の前に、CSS-in-JSについて少し解説します。
CSS-in-JSは、外部のCSSファイルではなく、Javascriptを用いてCSSを記述するスタイリング手法のことを指します。
CSS-in-JSを用いることで、CSSがコンポーネントファイルに定義され、外部のCSSファイルに依存することがないく、CSSの変更による影響をコンポーネント内に止めることができます。
また、JavaScriptの状態やpropsに基づいてスタイルを動的に変更できるのも大きなメリットです。

現在、App Routerディレクトリ内のクライアントコンポーネントに対応しているCSS in JSは以下のとおりです。

  • Chakra UI
  • Kuma UI
  • @mui/material
  • @mui/joy
  • Panda CSS
  • styled-jsx
  • styled-componentsv
  • vstylex
  • tamaguiv
  • tss-react
  • vanilla-extract

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

Runtime CSS-in-JS

Runtimeとは、プログラムの「実行時」のことであり、フロントエンド領域においては、JavaScriptが実行される時(つまりブラウザでページが読み込まれる時)にCSSが生成され適用されることを指します。
CSS-in-JSにおけるRuntimeの概念は非常に重要で、パフォーマンスや実装方法に大きく影響を与えます。

Runtime CSS-in-JS対応のライブラリ

  • emotion
  • styled-jsx
  • Styled Components
  • vstylex
  • tamaguiv
  • tss-react
  • chakra-ui

Runtime CSS-in-JSの利点

  • ローカルスコープのスタイル
    先ほども述べたように、CSSがコンポーネントファイルに定義され、外部のCSSファイルに依存することがないため、スタイルが意図せず広範囲に適用されるのを防ぐことができます。
    また、クラス名の衝突を避けられるというメリットもあります。

  • JavaScriptの変数の使用が可能
    こちらも先ほど述べましたが、JavaScriptの状態やpropsに基づいて、スタイルを動的に変更できるため、コードの重複を減らし、高度にカスタマイズ可能なコンポーネントを作成することができます。

  • コロケーション(関連コードの集約)
    コロケーションとは、1つのコンポーネントに関連するすべてのリソースを同じ場所に含めることです。
    これにより、コードの関連性が明確になり、管理が容易になるため、アプリケーションの保守性が向上します。

Runtime CSS-in-JSの課題

CSS-in-JSは多くの利点を提供する一方で、Runtimeオーバーヘッドという大きな課題を抱えています。

Runtime オーバーヘッドとは?

CSS-in-JSでは、コンポーネントがレンダリングされる際に、JavaScriptで記述されたスタイルをブラウザが解釈可能なプレーンCSSに変換(シリアライズ)する必要があります。この変換プロセスは、追加のCPUサイクルを消費します。さらに、生成されたCSSをDOMに挿入するための操作も必要となり、これもオーバーヘッドの一部となります。
propsや状態に基づいて動的にスタイルを変更する場合、これらの値が変わるたびにシリアライズ処理と挿入処理が必要になるため、さらなるオーバーヘッドを生み出す可能性があるのです。

https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b

Zero-Runtime CSS-in-JS

従来のRuntime CSS-in-JSでのオーバーヘッド問題を解決するものとして、Zero-Runtime CSS-in-JSが登場しました。
Zero-Runtime CSS-in-JSとは、JavaScript内にCSSを記述し、Build時に実際のCSSファイルを生成する手法を指します。
通常の CSS-in-jS と比較すると、JavaScriptでCSSを記述する利便性を維持しつつ、JavaScriptの実行時間を短縮できるという利点があります。

Zero-Runtime CSS-in-JS対応のライブラリ

  • Kuma UI
  • Linaria
  • vanilla-extract
  • Panda CSS
  • Goober
  • Astroturf
  • Treat

Zero-Runtime CSS-in-JSの利点

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

Zero-Runtime CSS-in-JSの課題

オーバーヘッドが発生せず、パフォーマンスの向上を図れる一方で、動的なスタイルを処理できないため、JavaScriptの強みである表現力が制限されてしまいます。

Hybrid CSS-in-JS

Runtime CSS-in-JSとZero-Runtime CSS-in-JSにの利点を組み合わせ、パフォーマンス上と動的スタイルの柔軟性をうまく両立させたハイブリッドな手法が、Hybrid CSS-in-JSです。

Hybrid CSS-in-JS対応のライブラリ

  • Kuma UI
  • Linaria
  • Stitches
  • Goober

Hybrid CSS-in-JSの利点

  • パフォーマンス
    Build時に可能な限り多くのCSSを静的に抽出することで、クライアントのブラウザの負荷を軽減し、アプリケーションを高速化します。

  • 柔軟性
    Runtime CSS-in-JSの表現力と動的機能を実現しながら、Zero-Runtim CSS-in-JSのパフォーマンス上の利点を享受できます。

  • 互換性
    React Server Components(RSC)との互換性があります。

Hybrid CSS-in-JSの課題

Hybrid CSS-in-JSでは、2つのアプローチを混在しているため、コードベースが複雑になったり、スタイルの優先順位や衝突が起こる可能性があります。
さらに、Zero-runtime CSS-in-JSと比較すると、Hybrid approachは動的スタイリングの柔軟性を提供する一方で、わずかながらRuntimeオーバーヘッドが発生します。
特に、動的に生成される部分のスタイルについては、実行時の処理が必要となり、これがパフォーマンスに影響を与える可能性があります。
そのため、利点と課題を理解した上で適切な使い分けを判断し、プロジェクトの要件に応じて適切なバランスを取ることが重要です。

https://www.kuma-ui.com/docs/Concepts/Hybrid

おわりに

本記事ではApp Router時代のZero-Runtimeについて深く説明させていただきました。
RSCが軸となった今、Zero-Runtime CSS-in-JSの深い理解が必要不可欠です。
改めてCSS-in-JSの実装方法を再考するとともに、App Routerの思想に合わせてより高度なパフォーマンスと効率性を実現出来るよう目指していきましょう。

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

Discussion