導入
株式会社サイバーエージェントの「2weeks Web フロントエンドエンジニア向け体験型インターンシップ」にて、SGEマンガ事業部 で 2 週間の就業型インターンに参加しました。
配属先は、 ジャンプTOON の Web 版を開発するチームでした。
初めての就業型インターンでしたが、非常に親身に接してくださり、今まで得難かった経験を存分に体験することができました。
この記事は、株式会社サイバーエージェント様による内容確認を経て公開しています。
この記事の概要
- 株式会社サイバーエージェントのインターンシップ「Specialized Boot Camp」において、ジャンプTOON Web の「全体SSRによる低キャッシュ率(約30%)がログイン時の全体パフォーマンスを下げている」課題に対し、Next.js v15 RC の Partial Prerendering (PPR) を用いた改善余地を技術検証しました(2025年9月)。
- 課題の本質(全ページが認証依存ヘッダー経由で動的化 → Full Route Cache 無効)を、チーム内ヒアリングと Build 結果分析で整理しました。
- 技術検証では PPR 適用条件(Dynamic API の Suspense 境界化、GraphQLクライアント動的/静的分離)を満たすパターンを構築し、ユーザーからの視覚的な定性的指標と、LCP/TTI など定量的指標の改善余地を確認しました。
- 「現状の構造をどの粒度で再分割すればキャッシュ拡大できるか」という設計ガイドと意思決定のログをチームの資産をすることを意識しました。
- 機能要件に注力していた学生エンジニアから、非機能要件へと目を向けられるエンジニアになるための貴重な経験となりました。
プロダクトの課題
ジャンプTOON の Web 版は Next.js の App Router をフル活用しており、サーバー側に処理を寄せたことによる課題がありました。
具体的には、サーバー側での認証時に Next.js の Dynamic APIs である cookies 関数を使うと、ページ全体が動的レンダリングになり、Full Route Cache が適用されず、デフォルトでページのレンダリング結果がキャッシュされなくなります。
ジャンプTOON Web では、ページ共通のレイアウトに認証を必要とする UI があり、利用規約などの静的であるべきページに関してもログイン時に関してはキャッシュを持てていませんでした。
チームの浅原さんによる以下の記事に課題感の詳細が記されています。
この記事でも触れられていますが、この課題を改善するために、 Next.js の新機能である Partial Prerendering (PPR) を活用することが検討されていました。
PPRの特性
Partial Prerendering (PPR) は、 Next.js 14 から実験的機能として追加されている新しいレンダリング手法で、以下のような特徴があります。
- 静的シェルと、 <Suspense> で Dynamic APIs が囲われた動的な部分からなる。
- 静的シェル部分は、動的な要素が入る穴を残した状態のままビルド時に生成される。リクエスト時には即時に返され、その後動的な要素が入る穴がストリーミングで埋まっていく。
- 静的シェル部分は、
"use cache"ディレクティブと付随する設定によって任意のタイミングで再検証させることができる。

引用元:
一旦PPRを導入してみる
PPRを導入する際は導入したいページ内 ( /app/layout.tsx 含む) で使われる、全てのDynamic APIsを<Suspense>で囲う必要がありました。
- /app/layout.tsxにある <SessionProvider> の内部の
cookies関数 - ページ共通のヘッダーとフッターの中のコンポーネントにある
useSearchParams()
これらを全部 <Suspense> で囲いました。
ここまでやると PPR でビルドが通ります。
- Build 結果
▲ Next.js 15.5.1-canary.37 (webpack) - Environments: .env.local - Experiments (use with caution): · ppr: "incremental" ✓ rdcForNavigations (enabled by experimental.ppr) ⨯ typedRoutes Creating an optimized production build ... ------------------------------------------------------------ Route (app) Size First Load JS ┌ ƒ / 19.7 kB 341 kB ------------------------------------------------------------ ├ ƒ /error 2.63 kB 177 kB ├ ƒ /friends-invitation/[missionFriendsInvitationId] 240 B 374 kB ├ ƒ /friends-invitation/[missionFriendsInvitationId]/apply 239 B 374 kB ├ ◐ /fsa 192 B 177 kB ├ ƒ /health 190 B 117 kB ├ ƒ /help 2.27 kB 177 kB ├ ƒ /help/contact 3.78 kB 207 kB ├ ƒ /help/contact/completed 1.99 kB 177 kB ├ ○ /icon.svg 0 B 0 B ├ ƒ /maintenance 1.87 kB 185 kB ├ ○ /manifest.webmanifest 190 B 117 kB ├ ƒ /me 17.6 kB 250 kB ------------------------------------------------------------ ├ ◐ /series/ranking/female 225 B 277 kB ├ ◐ /series/ranking/male 226 B 277 kB ├ ◐ /series/ranking/overall 226 B 277 kB ├ ◐ /series/ranking/trend 225 B 277 kB ------------------------------------------------------------ ƒ Middleware 68.5 kB ○ (Static) prerendered as static content ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content ƒ (Dynamic) server-rendered on demand
PPR導入でのパフォーマンス改善効果
作品詳細ページに PPR を導入してパフォーマンスの測定を行いました。
まずは実際の動作を確認してみましょう。
- PPR 導入前
- PPR 導入後
リロード時のスケルトンが不要になっていることがわかります。
- Lighthouse結果
指標 | PPR 導入前 | PPR 導入後 | 差分 | 結果 |
|---|---|---|---|---|
First Contentful Paint (FCP) | 705ms | 680ms | -25ms | 改善 |
Largest Contentful Paint (LCP) | 1225ms | 1103ms | -122ms | 改善 |
Speed Index | 750ms | 680ms | -70ms | 改善 |
Time to Interactive (TTI) | 1233ms | 1109ms | -124ms | 改善 |
Server Response Time | 440ms | 389ms | -51ms | 改善 |
Total Blocking Time (TBT) | 0ms | 0ms | 0ms | 変化なし |
Cumulative Layout Shift (CLS) | 0 | 0 | 0 | 変化なし |
Top ページにも PPR を導入しました。ここではビルド生成物のサイズを見てみました。
- PPR導入前
Route (app) Size
┌ ƒ / 19.7 kB- PPR導入後
Route (app) Size
┌ ƒ / 23.1 kBこの増分は、静的シェルを作ったことにより、リクエストごとのレンダリングサイズを節約できた分であると考えることができます。
ユーザー側からの体感と定量的な部分の両方でパフォーマンスの改善が得られました。
実際に導入する際のプロセス案
フェーズ1: 動的な部分をとりあえず大きく切り出して <Suspense> で囲う
当初は機能ごとにコンポーネントが分けられていました。例えば、急上昇・総合・男性・女性タブのコンポーネントと、動的なユーザー情報に基づくランキングデータの表示コンポーネントが一緒に含まれてる親コンポーネントがありました。
しかし、 PPR の作法に則るなら、動的なコンポーネントと静的なコンポーネントという基準で分けるべきであり、急上昇・総合・男性・女性タブのコンポーネントは、ユーザー情報を表示するコンポーネントと一緒にあるべきではありません。
まずは動的な部分をとりあえず大きく切り出して <Suspense> で囲うか、レイアウトとあまり要素が多くないシンプルなページをPPRにするのが良さそうでした。
フェーズ2: GraphQL クライアント分割
ジヤンプTOON Web では、 GraphQL を採用しており、データ取得する際は、 GraphQL の Fragment Colocation を活用し、ページ毎に 1 つのクエリを組み立てています。
しかし、ユーザー情報が必要なページで PPR を導入するためには、動的データ用 GraphQL クライアントと静的データ ( 認証不要データ ) 用クライアントで分けないと静的シェル用のデータをビルド時に取得できません。
ユーザー情報を含む動的な部分と、それ以外の静的なデータ取得してから静的シェルを構成する部分に分ける必要があるページにPPRを導入するには、認証データをヘッダーに含むクエリと含まないクエリに分ける必要がありました。
フェーズ3: Cached Components ( 旧 Dynamic IO )
Cached Components ( 旧 Dynamic IO ) は、動的な要素の境界を<Suspense>で作る PPR の発展として、静的な要素の境界を ```"use cache"``` ディレクティブで作る考え方です。
Next.jsのfetch関数は、プリレンダリング時に実行され、取得したデータを静的データにしようとするのがデフォルトの挙動となっています。
Cached Components は、fetch関数のデフォルトの挙動を反転させて、プリレンダリング時にフェッチを行わないようにします。
その結果、 "use cache" ディレクティブで明示した範囲を静的な要素とするシンプルなルールにすることができます。
PPR と Cached Components の合せ技として、動的なコンポーネントの中にさらに静的なコンポーネントをネストすることができます。( ネストされた静的部分のキャッシュを持つことができる。)
Dynamic IOではさらにこれを発展させ、Dynamic RenderingにStatic Renderingを入れ子にすることが可能となります。
export default function Page() { return ( <> ... {/* Static Rendering */} <Suspense fallback={<Loading />}> {/* Dynamic Rendering */} <DynamicComponent> {/* Static Rendering */} <StaticComponent /> </DynamicComponent> </Suspense> </> ); }
引用元:
PPR の注意点
静的シェルを大きく作りすぎると、以下のようなエラーがビルド時に出ました ( ビルド自体は通る。 )
Error: This rendered a large document (>512 kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.静的シェルを早く配信できるのが PPR の強みなのにも関わらず、静的シェルが大きすぎて配信に時間がかかるために、パフォーマンスを改善できていない or かえって下げてしまっているということです。
静的シェルに関しても、初期表示に関わる部分のみを含めるといった工夫が必要そうです。
検証で意識したこと
PPR が実験的機能で導入事例が乏しく、 AI との壁打ちも有効でなかったため、コンポーネント粒度・Suspense 配置・Dynamic API 残存有無・静的シェルサイズなど様々な変数を洗い出して対照実験をとにかく回すことを意識しました。
個人開発物とは比べ物にならない大きさのリポジトリに最初は戸惑いながらも、今まで培ってきた試行錯誤のやり方を一つ一つ適用していくことで、確実な成果に繋げることができました。
チームの中でも期待の技術として注目されてきた状況の中で、積極的にチームメンバーへのヒアリングを行い、ユーザー体験の本質的な改善につながるPPR の導入方法を模索しました。
また、検証過程・結果を再現性のある形で記録することで、チームの資産とすることを意識しました。
最後に
トレーナーのかぶさんからは書き尽くせないほどの手厚いサポートをいただきました。
また、 ジャンプTOON の Web チームの方々には、貴重なお時間をいただき検証に関するヒアリングをさせていただきました。非常に有意義なご意見をいただくことができ、成果の精度を高めることができました。
関わってくださった全ての方に感謝申し上げます。