こんにちは。プロダクトエンジニアの金子です。
新機能としてリリースされた汎用申請を担当しています。
本記事では、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>で囲むことで、準備ができたコンポーネントから順次表示できるようにしました。
コード例は以下の通りです。postとcommentsのデータ取得のブロッキングを解消しています。
変更前のコード:
// 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からuseでmetadataを取り出すことで、動的にメタデータを更新しています。
変更前のコード:
// 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を作りあげていく仲間を募集中です!
本記事のように、ユーザー体験の改善に積極的に取り組んでいます。
少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!