SmartHR Tech Blog

SmartHR 開発者ブログ

Streaming SSRによるFCPの改善

こんにちは。プロダクトエンジニアの金子です。
新機能としてリリースされた汎用申請を担当しています。

本記事では、Streaming Server-Side Rendering(Streaming SSR)によるFirst Contentful Paint(FCP)の改善について紹介します。

背景と課題

開発中のWebアプリケーションで、ページが読み込まれるまで白い画面が長く表示されていることがありました。計測をしてみたところ、SSRのブロッキングによりFCPが悪化していることがわかりました。

以下は、パフォーマンス改善前のシーケンス図です。メタデータとページ処理が実行されたのちにレスポンスを返していました。メタデータとページ処理の実行が終わるまでFirst Byteを返却できないため、TTFB(Time to First Byte:最初のバイトまでの時間)が遅くなっていました。

sequenceDiagram
    title パフォーマンス改善前

    participant Browser
    participant Server

    Browser->>Server: Request
    Server->>Server: Metadata 処理
    Server->>Server: Page 処理
    Server-->>Browser: Response (metadata + page)
    Note right of Browser: TTFB

Streaming SSRの導入

TTFBを改善するため、Streaming SSRを導入しました。Streaming SSRは、HTMLを一度に返すのではなく、準備ができた部分から順次クライアントに送信する仕組みです。ページとメタデータ取得のそれぞれをストリーミング対応することで、TTFBを改善しました。

ページ取得のストリーミング対応

awaitで同期的に処理していた非同期処理を<Suspense>で囲むことで、準備ができたコンポーネントから順次表示できるようにしました。

コード例は以下の通りです。postcommentsのデータ取得のブロッキングを解消しています。

変更前のコード:

// app/posts/[id]/page.jsx
export default async function Page({ params }) {
  // awaitでデータを取得するため、ここでブロッキングが発生
  const post = await fetchPost(params.id);
  const comments = await fetchComments(params.id);

  return (
    <div>
      <Post post={post} />
      <CommentList comments={comments} />
    </div>
  );
}

変更後のコード:

// app/posts/[id]/page.jsx
export default async function Page({ params }) {
  const postPromise = fetchPost(params.id);
  const commentsPromise = fetchComments(params.id);

  return (
    <div>
      <Suspense fallback={<div>Loading the post...</div>}>
        <RelatedContent promise={postPromise} />
      </Suspense>

      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments promise={commentsPromise} />
      </Suspense>
    </div>
  );
}

以下は、ページ取得のストリーミング対応後のシーケンス図です。ページ処理のブロッキングが解消されたことで、TTFBが少し改善しました。

sequenceDiagram
    title ページ取得のストリーミング対応後

    participant Browser
    participant Server

    Browser->>Server: Request
    Server->>Server: Metadata 処理
    Server-->>Browser: Metadata Response
    Server-->>Browser: Streamed Page Response
    Note right of Browser: TTFB

メタデータ取得のストリーミング対応

Note: Next.js 15.2よりmetadataのストリーミングがサポートされ、generateMetadataを使用してもブロッキングは発生しなくなりました(Next.js 15.2 - Streaming Metadata)。

generateMetadataのブロッキングを改善しました。読込中のメタデータを静的に返し、クライアントサイドでメタデータを動的に更新する方式に変更しました。

コード例は以下の通りです。変更前のコードでは、fetchMetadataでブロッキングが発生しています。変更後のコードでは、title"読み込み中..."で返し、ブロッキングを解消しています。また、metadataPromiseからusemetadataを取り出すことで、動的にメタデータを更新しています。

変更前のコード:

// app/posts/[id]/page.jsx
export async function generateMetadata({ params }) {
  // API呼び出しによってデータを取得
  const metadata = await fetchMetadata(params.id);

  return {
    title: metadata.title,
    // ... その他のメタデータ
  };
}

変更後のコード:

// app/posts/[id]/page.jsx
// 静的なメタデータを設定
export const metadata = {
  title: "読み込み中...",
};

// ページコンポーネント
export default async function Page({ params }) {
  // メタデータに必要な情報をfetch
  const metadataPromise = fetchMetadata(params.id);

  return (
    <div>
      <Suspense fallback={null}>
        <DynamicMetadata metadataPromise={metadataPromise} />
      </Suspense>
      {/* 他のコンポーネント */}
    </div>
  );
}

// クライアントサイドでメタデータを更新
"use client";
export function DynamicMetadata({ metadataPromise }) {
  const { title } = use(metadataPromise);
  useEffect(() => {
    document.title = title;
  }, [title]);

  return null;
}

以下は、ページとメタデータ取得のストリーミング対応後のシーケンス図です。メタデータ処理のブロッキングが解消されたことで、TTFBが更に改善しました。

sequenceDiagram
    title ページとメタデータ取得のストリーミング対応後

    participant Browser
    participant Server

    Browser->>Server: Request
    par
        Server-->>Browser: Streamed Metadata Response
        Server-->>Browser: Streamed Page Response
    end
    Note right of Browser: TTFB

FCPの改善効果

Streaming SSRの導入により、画面表示までの時間を大幅に改善することができました。3回の計測結果の平均値を比較すると、改善前の886msから改善後は16msとなり、98%もの改善を達成しました。

計測結果の詳細は以下の通りです。

計測回数 改善前 改善後
1回目 842ms 19ms
2回目 977ms 14ms
3回目 838ms 15ms
平均 886ms 16ms

課題と今後の展望

Next.js標準のStreaming metadataへの移行

Next.jsのバージョン更新が滞っていたため、独自実装でメタデータのストリーミング対応を行っていました。しかし、Next.js 15.2でStreaming metadataが標準機能としてサポートされたため、バージョン更新と併せて移行を行いたいと考えています。

アクセシビリティの改善

タイトルのストリーミングによって発生した読み上げ機能の問題解消に取り組んでいます。現状、メタデータが動的に決定されるため、スクリーンリーダーでのタイトル読み上げができなくなっています。改善前の実装ではメタデータをブロッキングして取得していたため読み上げが可能でした。

この問題に対して、aria-live属性を使用した解決を検討しています。タイトルが更新されたタイミングでスクリーンリーダーに通知できるようにしたいと考えています。

まとめ

Streaming SSRの導入により、FCPを大幅に改善し、ユーザー体験を向上させることができました。今後も継続的な改善を行い、より快適なWebアプリケーションを提供していきます。

We Are Hiring!

SmartHRでは一緒にSmartHRを作りあげていく仲間を募集中です!

本記事のように、ユーザー体験の改善に積極的に取り組んでいます。
少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!

hello-world.smarthr.co.jp