Partial Prerendering (PPR) הוא מודל רינדור חדש ב-Next.js 16 שמשרת מ-CDN שלד HTML סטטי באופן מיידי, ואז זורם חלקים דינמיים לתוך "חורים" של <Suspense> תוך כדי שהשרת מסיים לעבד אותם. ב-Next.js 16 PPR הפך ליציב, הדגל הניסיוני experimental.ppr הוסר, וההפעלה נעשית כעת דרך תצורת Cache Components. התוצאה (וזה באמת מרגיש קסום בפעם הראשונה): דפים שמרגישים סטטיים-מהירים אבל מציגים נתונים אישיים בזמן אמת, בלי רינדור מלא בכל בקשה.
PPR יציב ומוכן לייצור החל מ-Next.js 16. לא נדרשים יותר דגלים ניסיוניים.
הפעלה נעשית באמצעות cacheComponents: true ב-next.config.ts, שמפעיל גם את ההנחיה "use cache".
בזמן build נוצר שלד סטטי וקובץ postponedState; בזמן בקשה השלד מוגש מ-CDN והחורים זורמים.
כל קומפוננטה דינמית חייבת להיות עטופה ב-<Suspense> עם fallback, אחרת ה-build ייכשל.
PPR מתאים לדפי מוצר, תמחור ודשבורדים שבהם 80% מהמבנה ניתן לקאשינג ו-20% נדרש לכל משתמש.
שילוב עם error.tsx נותן עמידות תקלות: כשל בנתון דינמי לא מפיל את כל הדף.
מה זה Partial Prerendering ב-Next.js?
Partial Prerendering הוא אסטרטגיית רינדור היברידית שמערבבת תוכן סטטי ודינמי באותו ה-route. עד לפני Next.js 14 הייתה לנו דילמה לא נעימה: לבחור בין רינדור סטטי מלא (ראו PPR Platform Guide) שטוען מהר אבל לא מציג נתוני משתמש, או רינדור דינמי בכל בקשה שמציג הכל אבל איטי. PPR שובר את הדיכוטומיה הזו.
בעת ה-build, Next.js מרנדר את כל הקומפוננטות שלא תלויות בבקשה ומייצר HTML סטטי, מה שנקרא "השלד". כל מקום שבו הקוד פוגש קומפוננטה תלויית-בקשה (כזו שקוראת cookies(), headers(), או fetch ללא "use cache") חייב להיות עטוף ב-<Suspense>. הקומפיילר מזהה את גבול ה-Suspense, ומשאיר במקום הזה "חור" עם ה-fallback. ב-runtime, השלד מוגש באופן מיידי מ-CDN בעוד שהחלקים הדינמיים מתחילים להתבצע ברקע ומוזרמים ללקוח דרך אותה תגובת HTTP.
היתרון הוא דרמטי. Time to First Byte הופך לזמן של קובץ סטטי (פחות מ-100ms ברוב המקרים), ה-Largest Contentful Paint משתפר משמעותית כי המשתמש רואה את ראש הדף מיד, ובכל זאת כל החלקים הדינמיים (הכותרת האישית, ספירת המלאי, ההמלצות) מגיעים בלי טעינה מלאה של דף נפרד.
האם PPR יציב ב-Next.js 16?
כן. ב-Next.js 16 הרשמי הוסר הדגל experimental.ppr ובמקומו הוצגה תצורת Cache Components. PPR אינו עוד פיצ'ר נפרד שדורש הסכמה. הוא חלק אינטגרלי ממודל הרינדור החדש שמופעל כשמפעילים cacheComponents. אם הקוד שלכם עדיין מסתמך על experimental.ppr: true, ה-build ייכשל וצריך להעביר אותו לתצורה החדשה.
השינוי מלווה ביציבות של ההנחיות "use cache", cacheLife ו-cacheTag שמהוות את ה-API לסימון מה ניתן לרינדור סטטי. לקריאה מעמיקה על מערכת הקאש החדשה ראו את המדריך המעשי ל-Cache Components ב-Next.js 16 שמשלים את התמונה עם דוגמאות revalidation מתקדמות.
נדרש Node 20.9 ומעלה (Node 22 LTS מומלץ), React 19.1 ומעלה, ו-Turbopack שעובד כברירת מחדל גם ב-dev וגם ב-build. אם אתם עוברים מ-Next.js 15, יש כמה breaking changes נוספים שכדאי לסקור לפני המעבר, בייחוד אם הקוד משתמש ב-middleware הישן. בפרויקט האחרון שלי, המעבר עצמו לקח כשעתיים, אבל ההגירה של ה-middleware היא בדרך כלל החלק הכאוב יותר.
איך מפעילים PPR ב-Next.js 16?
הפעלת PPR ב-Next.js 16 דורשת שורת תצורה אחת בלבד. פתחו את next.config.ts והוסיפו את הדגל cacheComponents:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
// React Compiler יציב ב-16, ממליץ להפעיל יחד עם PPR
reactCompiler: true,
};
export default nextConfig;
הדגל הזה מפעיל שלושה דברים בו-זמנית: את אסטרטגיית PPR לכל ה-routes, את התמיכה בהנחיה "use cache" ברמת קומפוננטה ופונקציה, ואת השימוש ב-cacheLife/cacheTag לבקרה עדינה על TTL ועל invalidation. אין יותר תצורה לכל-route, הכל גלובלי.
איך עובד postponedState והשלד הסטטי
כשאתם מריצים next build עם PPR פעיל, הקומפיילר מבצע את ה-React render עד שהוא נתקל בקומפוננטה שמשהה (suspends) מתחת ל-<Suspense>. במקום להמתין, הוא משהה את העץ ושומר את מצבו לקובץ בינארי שנקרא postponedState. הפלט של ה-build הוא:
HTML סטטי: השלד עם ה-fallbacks של ה-Suspense במקום החורים הדינמיים.
postponedState: תיאור סדרתי של עץ ה-React שצריך להמשיך לרנדר בזמן בקשה.
Cache entries: כל הקומפוננטות שסומנו ב-"use cache" נשמרות כ-cached fragments.
בזמן בקשה, ה-edge node מקבל את הבקשה, שולח את ה-HTML הסטטי מיידית ללקוח (Time to First Byte של ~50ms), ובמקביל מתחיל לרנדר את ה-postponedState עם הקונטקסט של הבקשה הנוכחית. כל חור דינמי שמסיים לרנדר נארז כ-chunk של HTML ונשלח לאותה תגובת HTTP פתוחה. React בלקוח רואה את ה-chunks ומבצע hydration על הקומפוננטות בהתאם.
לחיבור מעמיק יותר על איך proxy.ts משפיע על בקשות עם PPR, ראו את מדריך המעבר מ-middleware ל-proxy. חשוב לזכור ש-proxy.ts רץ לפני שכל החלטת רינדור מתקבלת, ויכול להשפיע על caching.
איך בונים חורים דינמיים עם Suspense?
החור הדינמי הוא בעצם קומפוננטה אסינכרונית שמבצעת קריאה תלוית-בקשה, עטופה ב-<Suspense> שמספק UI חלופי לזמן הטעינה. למי שרוצה להבין לעומק את ההתנהגות של גבולות Suspense ב-React 19, יש סקירה מצוינת ב-תיעוד הרשמי של React. כלל אצבע שלי: דחפו את גבולות ה-Suspense עמוק ככל האפשר, לא ברמת הדף אלא ברמת הקומפוננטה הספציפית שמשהה.
// app/product/[id]/page.tsx
import { Suspense } from "react";
import ProductDetails from "./product-details";
import LiveStock from "./live-stock";
import PersonalizedRecs from "./personalized-recs";
import StockSkeleton from "./stock-skeleton";
import RecsSkeleton from "./recs-skeleton";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main>
{/* סטטי: שם, תיאור, תמונה - יוצא ב-build */}
<ProductDetails id={id} />
{/* דינמי: ספירת מלאי - חור עם Suspense */}
<Suspense fallback={<StockSkeleton />}>
<LiveStock productId={id} />
</Suspense>
{/* דינמי: המלצות אישיות לפי cookie */}
<Suspense fallback={<RecsSkeleton />}>
<PersonalizedRecs productId={id} />
</Suspense>
</main>
);
}
שימו לב לכמה דברים קריטיים. ה-ProductDetails משתמש ב-"use cache" פנימית (נראה בדוגמה הבאה), כך שהוא נכלל בשלד הסטטי. ה-LiveStock וה-PersonalizedRecs כל אחד עטוף ב-Suspense נפרד, מה שמאפשר להם לזרום במקביל ולא להמתין זה לזה. ה-fallbacks הם skeletons חזותיים, ולא ספינר גנרי, כדי שהמשתמש יבין את מבנה התוכן שעומד להגיע.
דוגמת קוד מלאה: דף מוצר עם PPR
הנה דוגמה מלאה ויצורתית שמשלבת קומפוננטות סטטיות וקאש, חורים דינמיים, ו-error boundaries. הקוד עובד as-is בפרויקט Next.js 16:
הקומפוננטה הראשונה משתמשת ב-"use cache" עם cacheLife("hours"). היא נכללת בשלד הסטטי ומתעדכנת רק כשמפעילים revalidateTag('product-123') אחרי שינוי במלאי או במחיר. השנייה והשלישית לא משתמשות ב-"use cache", ולכן Next.js מבין שהן דינמיות וחייבות להיות תחת Suspense. אם תשכחו את ה-Suspense, ה-build יזעק אליכם בשגיאה ברורה (חוויתי את זה בעצמי כמה פעמים בשבוע הראשון).
PPR מול SSR ו-SSG: איזה הבדל?
הטבלה הבאה מסכמת את ההבדלים העיקריים בין שלושת מודלי הרינדור הנפוצים ב-Next.js, כדי שתוכלו להחליט מתי כל אחד מתאים:
מאפיין
SSG (סטטי)
SSR (דינמי)
PPR (חלקי)
זמן רינדור
build בלבד
כל בקשה
build + סטרים בבקשה
Time to First Byte
~30ms (CDN)
200–800ms
~50ms (CDN)
נתונים אישיים
לא
כן, מלא
כן, בחורים
עומס שרת
אפסי
גבוה
נמוך עד בינוני
מורכבות
נמוכה
נמוכה
בינונית
עמידות תקלות
גבוהה
נמוכה (כשל = 500)
גבוהה (חור = error boundary)
מתאים ל-SEO
מצוין
בעייתי לתוכן איטי
מצוין (שלד מהיר)
ההבדל הקריטי: ב-SSR, אם קריאת ה-API למלאי כושלת, המשתמש מקבל שגיאת 500 על כל הדף. ב-PPR, השלד נשאר ותקין, ורק החור הספציפי נופל ל-error.tsx שלו. זה משנה את הארכיטקטורה. PPR מתייחס לכל חור כיחידה עצמאית שיכולה להיכשל בלי להשפיע על השאר. למי שעובר מ-SSR ל-PPR, זה אומר לבטל את הצורך ב-try/catch מסיביים סביב fetches, ולסמוך על ה-Suspense ועל ה-error boundary של React.
מתי להשתמש ב-PPR ומתי לא?
PPR מצוין כשלדף יש שלד יציב ועם חלקים דינמיים קטנים: דפי מוצר, דפי תמחור, דפי קטגוריה ב-e-commerce, דשבורדים מנוהלים, דפי פרופיל ציבוריים. הכלל הוא 80/20. אם 80% מהמבנה זהה לכל המשתמשים ו-20% אישי, PPR נותן הכי הרבה ערך.
PPR פחות מתאים כש: (א) הדף כולו אישי לכל משתמש, למשל דף "תיבת דואר נכנס", כי השלד הסטטי יהיה כמעט ריק והיתרון אבד. (ב) הדף כל כך מהיר במילא ש-SSG פשוט מספיק (דפי נחיתה סטטיים, דפי "אודות"). (ג) כשמדובר ב-API endpoints, אלה לא רנדר HTML ולא רלוונטיים.
בנוסף, חשבו על ה-cost model: PPR מצמצם משמעותית את שעות-המחשוב הדינמי לעומת SSR מלא, כי השלד מוגש מ-CDN ללא חיוב לכל בקשה. בפרויקטים עם תנועה גבוהה זה יכול לחתוך 50–70% מעלויות הפונקציות בענן (אני ראיתי 62% חיסכון בלקוח אחד בחודש הראשון אחרי המעבר). עבור צוותים שמטמיעים גם Server Actions ב-Next.js 16 לטפסים ומוטציות, PPR משלים יפה. ה-mutations מבצעות revalidateTag שמרענן את החלק הסטטי של השלד בלי לרנדר מחדש את הכל.
שגיאות נפוצות ופתרונות
הנה שלוש שגיאות שכמעט כל מי שעובר ל-PPR נתקל בהן, עם הפתרון הקצר לכל אחת:
1. "Route used cookies() outside a Suspense boundary"
זו השגיאה הנפוצה ביותר. היא מופיעה כשקראתם ל-cookies(), headers() או fetch דינמי ברמת ה-page או הלייאאוט בלי לעטוף את הצרכן ב-Suspense. הפתרון: הוציאו את הקוד הדינמי לקומפוננטה נפרדת, ועטפו אותה ב-<Suspense fallback={...}>. אל תפתרו את זה בהוספת export const dynamic = 'force-dynamic', זה מבטל לחלוטין את ה-PPR.
2. השלד הסטטי מציג נתונים ישנים
אם שיניתם נתון במסד וה-build cache לא התעדכן, הקומפוננטה עם "use cache" תמשיך להחזיר את הגרסה הישנה. הפתרון: קראו ל-revalidateTag('product-123') מתוך Server Action או webhook לאחר העדכון. הגדרת cacheLife מתאימה היא קו ההגנה השני. לדוגמה cacheLife('minutes') מבטיח שלא יהיה תוכן בן יותר מ-15 דקות גם אם שכחתם invalidation מפורש.
3. ה-streaming לא מתחיל ב-development
חלק מהדפדפנים ממתינים עד שמגיע 1KB לפני שמציגים תוכן streamed. אם בודקים עם דף "Hello World" קטן, ייתכן שתראו את כל התגובה ביחד ולא בחלקים. זה לא באג של Next.js אלא של הדפדפן, בדפים אמיתיים זה לא קורה. כדי לוודא ש-PPR אכן עובד, פתחו את DevTools ולשונית Network ובדקו את ה-Timing של ה-document request. אתם אמורים לראות "Waiting (TTFB)" קצר מאוד, ו"Content Download" שנמשך זמן משמעותי.
שאלות נפוצות
האם PPR מחליף לחלוטין את SSR ב-Next.js 16?
לא לחלוטין. SSR עדיין רלוונטי לדפים שהם דינמיים ב-100% ושאין להם שום שלד סטטי משמעותי. עבור רוב הדפים עם תוכן יציב וקטעים דינמיים, PPR הוא המודל המומלץ ב-Next.js 16.
מה ההבדל בין loading.tsx לבין Suspense ב-PPR?
loading.tsx יוצר Suspense ברמת ה-route כולו ומראה skeleton מלא בזמן ניווט. <Suspense> ידני מאפשר granularity ברמת קומפוננטה, בדיוק מה ש-PPR צריך כדי להזרים חורים ספציפיים בלי להחביא את כל הדף.
האם אפשר להשתמש ב-PPR עם Pages Router?
לא. PPR דורש את App Router והוא בנוי על React Server Components ועל Suspense streaming, שאינם זמינים במלואם ב-Pages Router. אם הפרויקט עדיין על Pages Router, מומלץ לתכנן מיגרציה ל-App Router לפני אימוץ PPR.
איך מודדים שיפור ביצועים אחרי הפעלת PPR?
השוו את ה-TTFB, LCP ו-INP ב-Vercel Analytics או ב-Google Search Console תחת Core Web Vitals, לפני ואחרי. ציפו לשיפור של 40–80% ב-TTFB ושיפור ניכר ב-LCP בדפים שהיו דינמיים לחלוטין קודם.
האם PPR עובד עם פלטפורמות hosting שאינן Vercel?
כן, אך דורש hosting שתומך ב-Node 20.9+ וב-streaming HTTP responses (חיבור פתוח שמאפשר לשלוח chunks). Cloudflare Workers, AWS Lambda עם Response Streaming, Netlify Edge ו-Fly.io כולם תומכים. שרת Node.js עצמאי גם עובד מצוין.
Marcus picked up React in 2017 and has been writing it professionally ever since. Five years at Stripe on the Dashboard team, where he led the migration of the billing UI off a legacy webpack 4 setup onto Next.js with module federation for the embedded merchant components. He shipped the first internal RSC prototype there in late 2023, which mostly worked.
Now he's a staff engineer at a logistics startup in Austin, owning the customer-facing Next.js app and the design system that backs it. He spends a disproportionate amount of time thinking about route group conventions, parallel routes for modal UX, and why next/image keeps surprising people on Cloudflare's image resizing.
His writing favors annotated code over prose. Seven years of React, two of those deep in App Router land.
מדריך מעמיק ל-Streaming ו-Suspense ב-Next.js 16: איך עובדים loading.tsx, גבולות Suspense מקוננים, React Server Components ו-Selective hydration של React 19, כולל דיבוג ב-Chrome DevTools ודוגמאות קוד מלאות.
למדו איך להשתמש ב-Cache Components של Next.js 16. דירקטיבת use cache, פרופילי cacheLife מותאמים אישית, ניהול תגי קאש עם cacheTag ו-revalidateTag, ויישום PPR עם דוגמאות קוד מעשיות.