
اگر برنامه Next.js روی لپتاپ درست کار میکند اما بعد از استقرار گاهی داده قدیمی نشان میدهد، مشکل لزوماً از هاست نیست. ممکن است هر نمونه برنامه کش خودش را داشته باشد، CDN نسخه دیگری نگه دارد یا پس از تغییر داده فقط یکی از لایهها باخبر شود. راهحل، «اضافهکردن Redis به همهچیز» نیست؛ ابتدا باید بدانید چه چیزی کجا کش شده و چه زمانی قرار است تازه شود.
این آموزش برای App Router و قابلیتهای Cache Components نوشته شده و قراردادها با مستندات Next.js بررسیشده در ۲۰ اکتبر ۲۰۲۶ تطبیق داده شدهاند. نمونهها عمداً یک سناریوی عمومی و غیرشخصی دارند. قبل از کپیکردن کد، نسخه نصبشده پروژه را بررسی کنید؛ تنظیمات نسخههای قدیمی ISR و دستورهای جدید use cache را بدون بررسی کنار هم قرار ندهید.
اول داده را براساس نیاز به تازگی دستهبندی کنید
مقاله بلاگ، فهرست عمومی دستهها و موجودی لحظهای فروشگاه نیاز یکسانی ندارند. برای هر داده بنویسید چه کسی آن را میبیند، چند وقت تغییر میکند و نمایش نسخه قدیمی چه پیامدی دارد. اگر چند دقیقه تأخیر در نمایش توضیح محصول قابل قبول است، کش میتواند مناسب باشد. اگر داده برای تأیید پرداخت یا موجودی نهایی استفاده میشود، تصمیم حساس باید از منبع معتبر تازه گرفته شود.
مهمترین خط قرمز، اطلاعات شخصی است. سبد خرید، سفارش، داشبورد و قیمت اختصاصی نباید با کلید عمومی میان کاربران مشترک شوند. افزودن شناسه کاربر به کلید هم جای کنترل مجوز را نمیگیرد؛ اول احراز هویت و اجازه دسترسی بررسی میشود، بعد داده خوانده میشود. کلید نادرست کش میتواند یک باگ عملکردی را به نشت اطلاعات تبدیل کند.
برای هر دسته، یک قرارداد ساده تعریف کنید: حداکثر کهنگی قابل قبول، رفتار هنگام قطع منبع داده، روش ابطال و مسئول اندازهگیری. اگر هیچکس نمیداند «نسخه قدیمی تا چه زمانی مجاز است»، تست نتیجه هم ممکن نیست. کش خوب داده را با قاعده بازاستفاده میکند؛ کش بیقاعده فقط خطا را دیرتر آشکار میکند.
لایههای کش را از هم جدا کنید
| لایه | نمونه داده | روش بررسی |
|---|---|---|
| مرورگر و ناوبری | دارایی ثابت و داده مسیر | بارگذاری مستقیم در برابر جابهجایی داخل برنامه |
| CDN یا پراکسی | پاسخ عمومی HTTP | هدر کش، سن پاسخ و تفاوت درخواستها |
| Next.js | نتیجه تابع یا بخش قابل کش | تگ، طول عمر، نسخه و نمونه سرویس |
| کش دامنه برنامه | نتیجه کوئری عمومی در Redis | کلید، TTL و رفتار خطا |
| منبع اصلی | رکورد قطعی دیتابیس | خواندن مستقیمِ مجاز برای تطبیق نتیجه |
پاککردن یک لایه، لایه دیگر را الزاماً پاک نمیکند. اگر پس از اصلاح دیتابیس هنوز متن قدیمی دیده میشود، پاسخ مستقیم سرور را با پاسخ CDN مقایسه کنید. اگر فقط ناوبری داخلی مشکل دارد، مسیر رندر و داده سمت کلاینت را بررسی کنید. خاموشکردن همه کشها برای همیشه معمولاً راهحل مناسبی نیست؛ برای تشخیص، هر بار یک لایه را جدا کنید.
در راهنمای رسمی Self-hosting، رفتار چندنمونهای و CDN جدا بررسی شده است. از نتیجه یک نمونه محلی نمیتوان درباره دو نمونه پشت توزیعکننده بار نتیجه گرفت. فایل یا حافظه داخل یک نمونه نیز صرفاً به دلیل ابریبودن هاست، خودکار به فضای مشترک تبدیل نمیشود.
نمونه Cache Components برای داده عمومی
در این مثال، یک endpoint ثابت و قابل اعتماد، فهرست مقالههای عمومی را برمیگرداند. آدرس از تنظیمات سرور میآید، نه از ورودی بازدیدکننده. endpoint نباید اطلاعات خصوصی یا نتیجه وابسته به کوکی برگرداند. این نمونه برای شناخت قرارداد کش است؛ برای داده واقعی برنامه، اعتبارسنجی schema را متناسب با همان قرارداد کامل کنید.
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = { cacheComponents: true };
export default config;
// app/articles/data.ts
import "server-only";
import { cacheLife, cacheTag } from "next/cache";
export async function getPublicArticles() {
"use cache";
cacheLife({ stale: 30, revalidate: 60, expire: 300 });
cacheTag("public-articles");
const endpoint = process.env.PUBLIC_ARTICLES_ENDPOINT;
if (!endpoint) throw new Error("Public articles endpoint is not configured");
const response = await fetch(endpoint, {
cache: "no-store",
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error("Public articles source failed");
const data: unknown = await response.json();
if (!Array.isArray(data) || data.length > 100) {
throw new Error("Unexpected public articles response");
}
return data.map((item: unknown) => {
if (!item || typeof item !== "object" ||
!("id" in item) || !("title" in item) ||
typeof item.id !== "string" || typeof item.title !== "string") {
throw new Error("Invalid public article");
}
return { id: item.id, title: item.title };
});
}
در اینجا fetch عمداً کش مستقل ندارد و نتیجه تابع با use cache نگه داشته میشود؛ بنابراین یک لایه اضافی و مبهم برای دریافت داده ایجاد نمیکنیم. پارامترهای cacheLife سه رفتار متفاوت برای تازگی کلاینت، بازاعتبارسنجی و انقضای قطعی دارند. این اعداد انتخاب آموزشیاند، نه مقدار پیشنهادی برای هر فروشگاه یا محصول.
برای استفاده، تابع را از یک Server Component فراخوانی و آرایه را به فهرست تبدیل کنید. در React متن را بهصورت معمول رندر کنید و برای عنوان مقاله به HTML خام نیازی نیست. اگر مسیر بخشهای پویا دارد، ساختار Suspense و محدودیتهای Cache Components را طبق نسخه پروژه رعایت کنید. Build و اجرای production را هم بررسی کنید؛ رفتار حالت توسعه مبنای نهایی اندازهگیری کش نیست.
ابطال کش پس از انتشار محتوا
وقتی محتوا تغییر میکند، منتظر TTL ماندن همیشه مناسب نیست. تگ، ارتباط معنایی میان ورودیهای کش را مشخص میکند. برای مثال همه فهرستهای عمومی مقاله میتوانند تگ مشترک داشته باشند. بهتر است نام تگ از دامنه برنامه بیاید، نه از یک رشته دلخواه که هر کاربر بتواند در endpoint عمومی ارسال کند و همه کش سایت را مرتب باطل کند.
برای یک webhook، احراز هویت و محدودیت نرخ لازم است. ابتدا امضا یا credential معتبر و مجوز رویداد را بررسی کنید، سپس تگ ازپیشتعریفشده را باطل کنید. نمونه زیر فقط خط اصلی بعد از این کنترلهاست و endpoint کامل webhook نیست. برای اقدام کاربر داخل Server Function، مسیر متفاوت updateTag ممکن است مناسب باشد؛ آن را با درخواست عمومی بیاحراز هویت جایگزین نکنید.
import { revalidateTag } from "next/cache";
// Run only after authenticating and authorizing the publishing event.
revalidateTag("public-articles", "max");
طبق مستندات revalidateTag، انتخاب پروفایل روی رفتار تازگی اثر دارد. ابطال به روش stale-while-revalidate ممکن است ابتدا نسخه قبلی را بدهد و در پسزمینه تازه کند؛ بنابراین تست نباید بیدلیل انتظار ناپدیدشدن فوری همه نسخههای قدیمی را داشته باشد. برای عملیات نیازمند خواندن فوری تغییر خود کاربر، قرارداد updateTag را بررسی کنید.