إطار Next.js

استراتيجيات التخزين المؤقت في Next.js

35 دقيقة الدرس 21 من 40

فهم التخزين المؤقت في Next.js

التخزين المؤقت هو أحد أقوى الميزات في Next.js 13+ مع App Router. توفر Next.js طبقات متعددة من التخزين المؤقت لتحسين الأداء وتقليل حمل الخادم. فهم آليات التخزين المؤقت هذه أمر بالغ الأهمية لبناء تطبيقات سريعة وفعالة.

أنواع التخزين المؤقت في Next.js

تنفذ Next.js أربع آليات رئيسية للتخزين المؤقت:

  • Request Memoization: إزالة تكرار الطلبات ضمن عملية عرض واحدة
  • Data Cache: الاحتفاظ بنتائج جلب البيانات عبر الطلبات
  • Full Route Cache: تخزين HTML المعروض وحمولة RSC في وقت البناء
  • Router Cache: ذاكرة التخزين المؤقت من جانب العميل للمسارات المزورة
مهم: يختلف سلوك التخزين المؤقت بين التطوير والإنتاج. يحدث التخزين المؤقت الأكثر قوة فقط في بنيات الإنتاج.

Request Memoization

يزيل Request Memoization تلقائياً تكرار طلبات fetch بنفس عنوان URL والخيارات ضمن عملية عرض واحدة:

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

async function getProductReviews(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  // هاتان المكالمتان تتم إزالة تكرارهما - يتم تقديم طلب واحد فقط
  const product = await getProduct(params.id);
  const reviews = await getProductReviews(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <ReviewsList reviews={reviews} />
    </div>
  );
}
نصيحة: يعمل Request Memoization فقط ضمن نفس شجرة العرض. يتم إزالة تكرار الطلبات في مكونات مختلفة أثناء نفس العرض تلقائياً.

Data Cache (Fetch Cache)

يحتفظ Data Cache بنتائج fetch عبر الطلبات والنشرات. بشكل افتراضي، يتم تخزين جميع طلبات fetch مؤقتاً إلى أجل غير مسمى:

// افتراضي: مخزن مؤقتاً إلى أجل غير مسمى
async function getStaticData() {
  const res = await fetch('https://api.example.com/static-data');
  return res.json();
}

// إلغاء الاشتراك في التخزين المؤقت
async function getDynamicData() {
  const res = await fetch('https://api.example.com/dynamic-data', {
    cache: 'no-store'
  });
  return res.json();
}

// إعادة التحقق بعد 60 ثانية
async function getRevalidatedData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 }
  });
  return res.json();
}

خيارات التخزين المؤقت

توفر Next.js عدة خيارات للتحكم في التخزين المؤقت للبيانات:

// فرض التخزين المؤقت (السلوك الافتراضي)
fetch(url, { cache: 'force-cache' });

// عدم التخزين المؤقت أبداً
fetch(url, { cache: 'no-store' });

// إعادة التحقق بعد X ثانية
fetch(url, { next: { revalidate: 3600 } });

// إعادة التحقق بناءً على العلامات
fetch(url, { next: { tags: ['products'] } });

Full Route Cache

يخزن Full Route Cache HTML المعروض وحمولة React Server Component في وقت البناء للمسارات الثابتة:

// app/blog/[slug]/page.tsx

// توليد المعاملات الثابتة في وقت البناء
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

// سيتم إنشاء هذه الصفحة بشكل ثابت في وقت البناء
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
تحذير: المسارات التي تستخدم دوال ديناميكية (cookies()، headers()، searchParams) لا يمكن تخزينها مؤقتاً بالكامل وسيتم عرضها ديناميكياً.

Router Cache (التخزين المؤقت من جانب العميل)

Router Cache هو ذاكرة تخزين مؤقت من جانب العميل في الذاكرة تخزن حمولة RSC للمسارات المزورة:

// يخزن Router Cache تلقائياً المسارات المزورة
// يمكنك التحكم في سلوكه باستخدام خيارات الموجه

// app/layout.tsx
import { Link } from 'next/link';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* prefetch: يتحكم في سلوك الجلب المسبق */}
        <Link href="/about" prefetch={true}>حول</Link>
        <Link href="/contact" prefetch={false}>اتصل</Link>
        {children}
      </body>
    </html>
  );
}

علامات التخزين المؤقت وإعادة التحقق

تسمح لك علامات التخزين المؤقت بإبطال إدخالات ذاكرة التخزين المؤقت المحددة عند الطلب:

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  });
  return res.json();
}

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag');

  if (tag) {
    revalidateTag(tag);
    return Response.json({ revalidated: true, now: Date.now() });
  }

  return Response.json({ revalidated: false }, { status: 400 });
}

إعادة التحقق المستندة إلى المسار

يمكنك أيضاً إعادة التحقق من مسارات محددة:

// app/api/revalidate-path/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path');

  if (path) {
    revalidatePath(path);
    return Response.json({ revalidated: true, path });
  }

  return Response.json({ revalidated: false }, { status: 400 });
}

// الاستخدام: POST إلى /api/revalidate-path?path=/products

إعادة التحقق المستندة إلى الوقت

تكوين إعادة التحقق التلقائي على مستوى جزء المسار:

// app/products/page.tsx

// إعادة التحقق من هذه الصفحة كل 60 ثانية
export const revalidate = 60;

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json());

  return (
    <div>
      <h1>المنتجات</h1>
      <ProductList products={products} />
    </div>
  );
}

إلغاء الاشتراك في التخزين المؤقت

عدة طرق لإلغاء الاشتراك في التخزين المؤقت للمحتوى الديناميكي:

// الطريقة 1: استخدام الدوال الديناميكية
import { cookies } from 'next/headers';

export default async function DynamicPage() {
  const cookieStore = cookies();
  // هذا يجعل الصفحة ديناميكية
  return <div>محتوى ديناميكي</div>;
}

// الطريقة 2: تعيين تكوين جزء المسار الديناميكي
export const dynamic = 'force-dynamic';

// الطريقة 3: استخدام cache: 'no-store' في fetch
async function getData() {
  const res = await fetch(url, { cache: 'no-store' });
  return res.json();
}

// الطريقة 4: تعطيل التخزين المؤقت للمسار بأكمله
export const fetchCache = 'force-no-store';
خيارات تكوين جزء المسار:
  • dynamic: 'auto' | 'force-dynamic' | 'error' | 'force-static'
  • revalidate: false | number (بالثواني)
  • fetchCache: 'auto' | 'force-cache' | 'only-cache' | 'force-no-store'
  • runtime: 'nodejs' | 'edge'

أفضل ممارسات التخزين المؤقت

// 1. تخزين المحتوى الثابت بقوة
async function getStaticContent() {
  return fetch('https://api.example.com/static', {
    next: { revalidate: false } // تخزين إلى أجل غير مسمى
  });
}

// 2. استخدام أوقات إعادة تحقق مناسبة
async function getBlogPosts() {
  return fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // إعادة التحقق كل ساعة
  });
}

// 3. استخدام العلامات للمحتوى ذي الصلة
async function getProductData(id: string) {
  const [product, reviews, related] = await Promise.all([
    fetch(`https://api.example.com/products/${id}`, {
      next: { tags: ['products', `product-${id}`] }
    }),
    fetch(`https://api.example.com/products/${id}/reviews`, {
      next: { tags: [`product-${id}-reviews`] }
    }),
    fetch(`https://api.example.com/products/${id}/related`, {
      next: { tags: ['products', 'related-products'] }
    })
  ]);

  return { product, reviews, related };
}

// 4. عدم تخزين البيانات الخاصة بالمستخدم
async function getUserData(userId: string) {
  return fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store' // دائماً طازج
  });
}

تصحيح سلوك التخزين المؤقت

توفر Next.js رؤوس للمساعدة في تصحيح التخزين المؤقت:

// app/api/debug/route.ts
export async function GET() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 60, tags: ['debug'] }
  });

  return Response.json({
    data: await data.json(),
    headers: {
      'x-vercel-cache': data.headers.get('x-vercel-cache'),
      'cache-control': data.headers.get('cache-control'),
    }
  });
}
رؤوس حالة التخزين المؤقت:
  • HIT: البيانات المقدمة من ذاكرة التخزين المؤقت
  • MISS: البيانات المجلوبة من المصدر، مضافة إلى ذاكرة التخزين المؤقت
  • STALE: البيانات القديمة المقدمة، إعادة التحقق في الخلفية
  • BYPASS: تجاوز ذاكرة التخزين المؤقت (cache: 'no-store')

تمرين تطبيقي

المهمة: بناء موقع أخبار مع تخزين مؤقت مناسب:

  1. إنشاء صفحة رئيسية تخزن لمدة 5 دقائق
  2. يجب إنشاء المقالات الفردية بشكل ثابت في وقت البناء
  3. تنفيذ مسار API لإعادة التحقق من المقالات حسب العلامة
  4. يجب عدم تخزين تعليقات المستخدم مؤقتاً أبداً
  5. إضافة تصحيح حالة التخزين المؤقت إلى مسارات API الخاصة بك

إضافي: تنفيذ قسم "أحدث الأخبار" يتحدث كل دقيقة بينما يتحدث المحتوى الرئيسي كل 5 دقائق.

الملخص

النقاط الرئيسية حول التخزين المؤقت في Next.js:

  • توفر Next.js أربع طبقات تخزين مؤقت: Request Memoization وData Cache وFull Route Cache وRouter Cache
  • استخدم إعادة التحقق المستندة إلى الوقت للمحتوى الذي يتحدث بانتظام
  • استخدم إعادة التحقق المستندة إلى العلامات لإبطال ذاكرة التخزين المؤقت عند الطلب
  • يجب تخزين المحتوى الثابت بقوة، ولا ينبغي تخزين البيانات الخاصة بالمستخدم
  • استخدم خيارات تكوين جزء المسار للتحكم في سلوك التخزين المؤقت على مستوى المسار
  • راقب رؤوس حالة التخزين المؤقت لتصحيح مشكلات التخزين المؤقت