در ۷۲ ساعت گذشته چیز مهمی منتشر نشد که قبلاً پوشش نداده باشیم، پس امروز یک نکتهٔ حرفهای داریم و این بار نوبت Next.js است. همهٔ مطالب زیر با مستندات فعلی Next.js 16.3.x دربارهٔ caching و use cache تطبیق داده شده است.
نکته: در Cache Components عمقی که در آن روی دادههای زمان درخواست await میکنید تعیین میکند چه مقدار از مسیر پیشرندر میشود. اگر params، cookies() یا یک fetch بدون کش را در بالای یک layout با await بخوانید، کل زیردرخت کارِ زمان درخواست میشود. همان await را سه کامپوننت پایینتر انجام دهید، بقیه در static shell میماند.
آمادهسازی
Cache Components بهصورت اختیاری فعال میشود:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
با فعال بودن آن، پیشرندر مدل پیشفرض است. طبق مستندات، هر کامپوننت در یکی از چند دسته میافتد:
- محاسبهٔ خالص، importهای ماژول و I/O همگام در زمان build کامل میشوند و خودکار وارد shell میشوند.
- نتیجهٔ
use cacheوارد shell میشود، به شرط آنکه طول عمرش بیش از حد کوتاه نباشد. - کار async بدون کش و APIهای زمان درخواست (
cookies،headers،searchParamsوparamsپویا) باید پشت<Suspense>باشند. fallback وارد shell میشود و محتوا در زمان درخواست استریم میشود. - فراخوانیهای غیرقطعی مثل
Math.random()،Date.now()یاcrypto.randomUUID()یاconnection()بهعلاوهٔ<Suspense>میخواهند یاuse cache.
طبق مستندات، وقتی چیزی در پیشرندر کامل نمیشود و نه boundary دارد نه کش، Next.js یک insight اعتبارسنجی (insight با عنوان «blocking route» در dev overlay) نشان میدهد. این همان علامتی است که این نکته به آن مربوط است.
اشتباه رایج
یک layout که param پویا را در بالا میخواند:
// app/shop/[slug]/layout.tsx
export default async function Layout({
children,
params,
}: LayoutProps<"/shop/[slug]">) {
const { slug } = await params;
return (
<div>
<Sidebar />
<h1>{slug}</h1>
{children}
</div>
);
}
اگر slug را generateStaticParams تأمین نکند، دادهای زمان درخواست است. چون خود layout آن را await میکند، layout پیشرندر نمیشود و Sidebar هم که ربطی به param ندارد، پیشرندر نمیشود. مستندات دقیقاً همین مثال را آوردهاند.
راهحل
layout را async نکنید. promise را پایین بدهید، یا داخل یک boundary با .then() بخوانید:
import { Suspense } from "react";
export default function Layout({
children,
params,
}: LayoutProps<"/shop/[slug]">) {
return (
<div>
<Sidebar />
<Suspense fallback={<h1>Loading...</h1>}>
{params.then(({ slug }) => (
<SlugHeading slug={slug} />
))}
</Suspense>
{children}
</div>
);
}
function SlugHeading({ slug }: { slug: string }) {
return <h1>{slug}</h1>;
}
حالا Sidebar، children و fallback بخشی از shell هستند و فقط SlugHeading استریم میشود. مستندات تصریح میکند که همین اصل برای cookies()، headers()، searchParams و واکشی داده هم صادق است.
سپس بر اساس آرگومان کش کنید
وقتی خواندن به پایین درخت رفت، میتوانید به بخش گران هم طول عمر بدهید. یک scope از نوع use cache نمیتواند خودش cookies() یا headers() را صدا بزند و این محدودیت به helperهایی که صدا میزند هم میرسد. الگوی ترجیحی مستندات این است که مقدار زمان درخواست را بیرون بخوانید و بهعنوان آرگومان بدهید:
import { cookies } from "next/headers";
import { Suspense } from "react";
import { cacheLife } from "next/cache";
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProfileContent />
</Suspense>
);
}
async function ProfileContent() {
const session = (await cookies()).get("session")?.value;
return <CachedContent sessionId={session} />;
}
async function CachedContent({ sessionId }: { sessionId?: string }) {
"use cache";
cacheLife("minutes");
const data = await fetchUserData(sessionId);
return <div>{data}</div>;
}
آرگومانها و متغیرهای captureشده از scope بیرونی جزو کلید کش میشوند، پس هر sessionId ورودی جداگانه دارد. تعیین صریح cacheLife توصیه میشود؛ بدون آن پروفایل default اعمال میشود (stale پنج دقیقه در کلاینت، revalidate پانزده دقیقه در سرور، بدون انقضای زمانی).
بدهبستانها و نکات احتیاط
- در serverless ممکن است کش hit نشود. محل ذخیرهٔ پیشفرض حافظهٔ درونفرایندی است. در serverless ورودیها معمولاً بین درخواستها نمیمانند و تابع کششده ممکن است دوباره اجرا شود. Node خودمیزبان ورودیها را نگه میدارد و
cacheMaxMemorySizeاندازه را محدود میکند.use cache: remotehandler مشترک میدهد، به قیمت یک رفتوبرگشت شبکه و معمولاً هزینهٔ پلتفرم. مستندات میگوید فقط در نرخ hit بالا میارزد. - کامپوننت کششده بهازای هر session در static shell نیست. پشت دادههای درخواست قرار دارد. چیزی که به دست میآورید مسیر prefetch است: با فعال بودن Partial Prefetching، طول عمر باعث میشود نتیجه وارد prefetch شود.
- دادن promise به تابع کششده میتواند build را قفل کند. اگر
cookies()(خود promise) یا promiseهای زمان درخواست دیگر را بهصورت props به تابعuse cacheبدهید، پیشرندر تا timeout پنجاهثانیهای منتظر میماند. بیرونawaitکنید و مقدار را بدهید. مستندات همین خرابی را برای promiseهایی که در یکMapمشترک نگهداری شوند هم نشان میدهد. React.cacheاز مرز عبور نمیکند. داخلuse cachescope جداگانه دارد، پس مقدارهای بیرونی دیده نمیشوند. از آرگومان استفاده کنید.- ورودیهای کش با deploy جدید نمیمانند. شناسهٔ build (یا
deploymentId) در کلید است. - رباتها رندر کامل پویا میگیرند. طبق مستندات، خزندهها shell را رد میکنند و HTML کامل رندرشده در زمان درخواست میگیرند؛ پس هر چیز در shell که به ورودیهای فقط-build وابسته است باید در زمان درخواست هم کار کند.
- APIهای درخواست در کد کششده میتوانند از
next buildرد شوند و بعداً خطا بدهند در مسیرهای پویا، چون خطا وقتی مسیر اجرا شود پدیدار میشود.
وقتی الگوی اولیه اشکالی ندارد
اگر layout سبک است، همزادهای کمی دارد، یا مسیر ذاتاً پویاست، await در بالا هزینهٔ کمی دارد و شکستن آن فقط لایهٔ غیرمستقیم اضافه میکند. بازآرایی وقتی میارزد که مقدار منتظرشده را بخش کوچکی از یک زیردرخت بزرگ استفاده کند. دادههای قابلپیشبینی، مثل فایل پیکربندی که تغییر نمیکند، به هیچیک از اینها نیاز ندارد: در سطح ماژول بخوانیدش تا خودش پیشرندر شود.
چکلیست سریع
cacheComponentsرا روشن کنید و dev overlay را روی سنگینترین مسیر باز کنید.- برای هر insight از نوع blocking-route، کمعمقترین
awaitروی دادههای زمان درخواست را پیدا کنید. - آن را به کوچکترین کامپوننتی که به مقدار نیاز دارد، پشت
<Suspense>ببرید. - اگر آن کامپوننت گران است و مقدار ورودیهای متمایز کمی دارد، مقدار را بیرون بکشید و به تابع
use cacheباcacheLifeصریح بدهید.