آیا Partial Pre Rendering در Next.js خوبه؟ روی ۱۰۰۰ صفحه تست کردم
حدود ۸ دقیقه مطالعه
اگه این روزها با Next.js کار کرده باشی، حتماً اسم PPR یا همون «پیشرندر جزئی» به گوشت خورده. ایدهاش واقعاً جذابه: بخش ثابت صفحه همون لحظه نمایش داده بشه و بخشهای داینامیک بعداً استریم بشن. من روی یه پروژه با حدود ۱۰۰۰ صفحه بلاگ و داکیومنت تستش کردم. نتیجه اون چیزی نبود که انتظار داشتم، و این مقاله داستان همین تسته.
اول ببینیم PPR اصلاً چیه
قبلاً برای هر صفحه باید یکی از دو راه رو انتخاب میکردی:
- استاتیک کامل: صفحه موقع بیلد ساخته میشه و خیلی سریعه، ولی بخشهای شخصیسازیشده توش جا نمیشن.
- داینامیک کامل: صفحه برای هر درخواست از نو رندر میشه. همیشه تازهست، ولی کندتره و بار بیشتری روی سرور میذاره.
ترکیب این دو، کار PPR ـه. موقع بیلد یه «پوسته» استاتیک از صفحه ساخته میشه که شامل هدر، منو، فوتر و محتوای ثابته. هر جایی که داده داینامیک لازم باشه، یه «حفره» میمونه که موقع درخواست پر میشه و بهصورت استریم به مرورگر میرسه.
وضعیت فعلی PPR در نسخه ۱۶
در Next.js 16 دیگه PPR یه فلگ جدا نیست. حالا جزئی از مدل جدیدی به اسم Cache Components حساب میشه و با این تنظیم فعال میشه:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
مهمترین تغییر اینه که این سوییچ سراسریه. توی نسخه ۱۵ میشد PPR رو صفحهبهصفحه روشن کرد، ولی در نسخه ۱۶ اون فلگهای قدیمی حذف شدن. وقتی روشنش کنی، مدل PPR برای کل App Router فعال میشه. پایینتر میگم این موضوع چه تأثیری روی تصمیمگیری داره.
تستی که من گرفتم
پروژه تست من حدود ۱۰۰۰ صفحه داشت که ترکیبی از پستهای بلاگ و صفحههای داکیومنت بود. اپ رو بهصورت سلفهاست بالا آوردم و دو حالت رو مقایسه کردم:
- با PPR روشن: مصرف رم پروسه Node به حدود ۱.۵ گیگابایت رسید و حتی بعد از تموم شدن درخواستها هم پایین نیومد.
- با PPR خاموش: همون اپ با همون صفحهها حدود ۱۵۰ مگابایت رم مصرف کرد.
یعنی ده برابر رم بیشتر، برای سایتی که محتواش عملاً ثابته. برای یه VPS کوچیک، این فرق بین «راحت اجرا میشه» و «هر چند وقت یه بار کرش میکنه» ـه.
چرا رم آزاد نمیشه؟
برای این رفتار دو دلیل محتمل وجود داره:
- کش درونحافظهای: در مدل Cache Components، خروجیهای کششده بهصورت پیشفرض توی رم سرور و در یه کش LRU نگه داشته میشن. مستندات خود Next.js میگه در حالت سلفهاست این ورودیها بین درخواستها باقی میمونن. وقتی ۱۰۰۰ صفحه داری، این کش خیلی سریع بزرگ میشه.
- رفتار حافظه در V8: حتی وقتی یه آبجکت آزاد میشه، موتور جاوااسکریپت معمولاً حافظه رو سریع به سیستمعامل پس نمیده. برای همین عددی که مانیتورینگ نشون میده ممکنه بالا بمونه.
نتیجه اینه که هر چی تعداد صفحهها بیشتر باشه، این هزینه محسوستر میشه.
بلاگ و داکیومنت اصلاً به PPR نیاز دارن؟
جواب کوتاه: معمولاً نه.
محتوای یه پست بلاگ یا یه صفحه داکیومنت برای همه کاربرها یکیه. حفره داینامیکی هم وجود نداره که بخواد استریم بشه. برای این نوع صفحهها همون تولید استاتیک کلاسیک کاملاً کافیه:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <article>{/* محتوای پست */}</article>
}
با این روش صفحهها موقع بیلد ساخته میشن و سرور فقط HTML آماده تحویل میده. نه کش سنگینی لازمه، نه رم اضافهای مصرف میشه.
کجا PPR واقعاً میدرخشه
قدرت PPR وقتی معلوم میشه که یه صفحه هم بخش ثابت داره، هم بخش شخصی. چند تا مثال:
- صفحه محصول فروشگاه: توضیحات و عکسها ثابتن، ولی سبد خرید، موجودی انبار یا قیمت مخصوص کاربر داینامیکه.
- داشبورد کاربری: منو و ساختار صفحه ثابته، ولی اطلاعات حساب و آمار هر کاربر فرق میکنه.
- صفحه اصلی با پیشنهاد شخصی: بنر و بخشهای عمومی ثابتن، ولی پیشنهادها برای هر کاربر متفاوته.
در این حالت بخش داینامیک رو داخل Suspense میذاری و بقیه صفحه بلافاصله نمایش داده میشه:
// app/product/[id]/page.tsx
import { Suspense } from 'react'
export default function ProductPage() {
return (
<main>
<ProductInfo />
<Suspense fallback={<CartSkeleton />}>
<UserCart />
</Suspense>
</main>
)
}
اینجا کاربر محصول رو همون لحظه میبینه و سبد خریدش چند صدم ثانیه بعد ظاهر میشه. این همون تجربهایه که PPR براش ساخته شده.
چطور PPR رو فقط جایی که لازمه استفاده کنیم
پیشنهاد من سادهست: PPR رو روی کل سایت روشن نکن. فقط برای صفحههایی ازش استفاده کن که واقعاً بخش داینامیک دارن. چون سوییچش در نسخه ۱۶ سراسریه، برای این کار چند راه داری.
راه اول: جدا کردن بخشها با مولتیزون
بلاگ و داکیومنت رو در یه اپ Next.js جدا با PPR خاموش نگه دار، و بخش داینامیک مثل داشبورد یا فروشگاه رو در یه اپ دیگه با PPR روشن. اپ اصلی درخواستهای بلاگ رو به اپ دوم میفرسته:
// next.config.ts در اپ اصلی (PPR روشن)
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
async rewrites() {
return [
{ source: '/blog', destination: 'http://localhost:3001/blog' },
{ source: '/blog/:path+', destination: 'http://localhost:3001/blog/:path+' },
{ source: '/blog-static/:path+', destination: 'http://localhost:3001/blog-static/:path+' },
]
},
}
export default nextConfig
// next.config.ts در اپ بلاگ (PPR خاموش)
const nextConfig = {
assetPrefix: '/blog-static',
}
export default nextConfig
اینطوری هزاران صفحه استاتیک هیچ هزینهای از بابت PPR نمیدن.
راه دوم: اگه هنوز روی نسخه ۱۵ هستی
در نسخه ۱۵، که اونجا هم فقط روی نسخههای canary در دسترس بود، میشد PPR رو بهصورت تدریجی روشن کرد:
// next.config.ts (Next.js 15 canary)
const nextConfig = {
experimental: {
ppr: 'incremental',
},
}
export default nextConfig
بعد فقط در صفحههایی که لازمه این خط رو اضافه میکردی:
// app/dashboard/page.tsx
export const experimental_ppr = true
دقت کن که این روش در نسخه ۱۶ حذف شده. پس اگه قصد آپگرید داری، از الان روی راه اول یا سوم برنامهریزی کن.
راه سوم: اگه مجبوری روشن نگهش داری
اگه کل پروژه روی Cache Components رفته، حداقل مصرف رم رو کنترل کن. اولین قدم محدود کردن سایز کش درونحافظهایه:
// next.config.ts
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
cacheMaxMemorySize: 50 * 1024 * 1024, // حدود ۵۰ مگابایت
}
export default nextConfig
قدم بعدی اینه که اگه چند اینستنس داری یا کش بزرگ لازمه، کش رو از رم بیرون ببری. برای این کار میتونی یه cache handler مثل Redis تعریف کنی و برای دادههای سنگین از این دایرکتیو استفاده کنی:
async function getHeavyData() {
'use cache: remote'
// ...
}
قدم آخر هم تعیین سقف حافظه برای پروسه Node ـه، که جلوی رشد بیحد رم رو میگیره:
NODE_OPTIONS="--max-old-space-size=512" next start
یه نکته امنیتی مهم
در سال ۲۰۲۶ چند آسیبپذیری مرتبط با PPR منتشر شد. مهاجم با فرستادن درخواستهای مخصوص به مسیر «ادامه رندر» میتونست رم سرور رو پر کنه و پروسه رو از کار بندازه. این مشکل در نسخه 16.1.7 بهطور کامل رفع شده. اگه PPR یا Cache Components رو روشن داری، حتماً حداقل به این نسخه آپگرید کن:
npm install next@latest
اگه فعلاً نمیتونی آپگرید کنی، درخواستهایی رو که این هدر رو دارن در ریورس پراکسی (مثلاً Nginx) مسدود کن، چون این هدر هیچوقت نباید از سمت کاربر بیاد:
if ($http_next_resume) {
return 403;
}
جمعبندی: ارزشش رو داره یا نه؟
جواب: بستگی داره. PPR یه ابزار قدرتمنده، ولی راهحل همهچیز نیست.
- برای سایتهای محتوامحور مثل بلاگ، داکیومنت و لندینگ، معمولاً ارزشش رو نداره. تولید استاتیک کلاسیک همون سرعت رو با یکدهم رم بهت میده.
- برای صفحههای ترکیبی مثل فروشگاه، داشبورد یا صفحههایی با محتوای شخصی، واقعاً تجربه کاربری رو بهتر میکنه.
- برای پروژههای بزرگ، بهترین کار جدا کردن بخشهاست تا هزینه PPR فقط برای صفحههایی پرداخت بشه که ازش سود میبرن.
قبل از روشن کردن هر فیچر جدید، روی پروژه خودت تستش کن و رم و زمان پاسخ رو اندازه بگیر. عدد ۱.۵ گیگابایت در برابر ۱۵۰ مگابایت به من یاد داد که «جدیدترین» همیشه به معنی «بهترین برای پروژه من» نیست.
