Next.js 16 شرح Streaming: Suspense + use cache

١٢ مايو ٢٠٢٦

Next.js 16 Streaming Tutorial: Suspense + use cache 2026

أصبح البث (Streaming) في Next.js 16 أخيراً قصة واحدة متماسكة بدلاً من ثلاث واجهات برمجة تطبيقات (APIs) غير مكتملة. يمكنك تغليف مكون خادم غير متزامن (async Server Component) بداخل <Suspense>، فيقوم الخادم بإرسال الهيكل الأساسي (shell) عبر ترميز نقل البيانات المجزأ (chunked transfer encoding)، ثم يقوم بديل (fallback) سريع البث بتسليم المحتوى الحقيقي بمجرد اكتمال كل استعلام1. ما تغير في الإصدار 16.2 هو جانب التخزين المؤقت (caching) من الصورة: أصبحت use cache، و cacheLife، و cacheTag، و updateTag الآن مستقرة، وتمت إزالة البادئة unstable_، وأصبح Turbopack هو مجمع الحزم (bundler) الافتراضي لكل من dev و build2. يربط هذا البرنامج التعليمي لعام 2026 تلك الأساسيات في مشروع واحد قابل للتشغيل حتى تتمكن من بث صفحة بطيئة، وتخزين الأجزاء التي تستفيد من ذلك فقط مؤقتاً، وإبطال التخزين المؤقت عند تنفيذ Server Action، ومشاهدة آلية "اقرأ ما كتبته" (read-your-writes) وهي تعمل من البداية للنهاية.

ملخص

ستقوم ببناء صفحة "منشورات مباشرة" صغيرة في Next.js 16.2.6 تقوم ببث قسمين مستقلين يتم عرضهما على الخادم، وتعتمد على loading.tsx كبديل لهيكل المسار و <Suspense> للبطاقات المتجاورة، وتختار نموذج التخزين المؤقت الجديد باستخدام cacheComponents: true، وتميز جالب البيانات البطيء بـ 'use cache' مع ملف تعريف cacheLife('minutes')، وتنتهي بـ Server Action يستدعي updateTag بحيث يظهر التعديل الذي أجراه المستخدم في العرض التالي مباشرةً. إجمالي وقت البناء: 25-35 دقيقة. لا توجد خدمات خارجية مطلوبة؛ كل شيء يعمل على localhost.

ما ستتعلمه

  • كيف يعمل البث فعلياً في Next.js 16 — ما يرسله الخادم، وما يفعله المتصفح، ولماذا يعتبر Suspense هو الحدود الفاصلة الأهم
  • كيفية الاختيار بين loading.tsx و <Suspense> للبدائل على مستوى المسار مقابل مستوى المكون
  • كيفية تمييز مكون خادم بطيء كقابل للتخزين المؤقت باستخدام توجيه 'use cache'
  • كيفية التحكم في عمر التخزين المؤقت باستخدام ملفات تعريف cacheLife والوسم للإبطال باستخدام cacheTag
  • كيفية إبطال التخزين المؤقت الصحيح من Server Action باستخدام updateTag مقابل revalidateTag
  • كيفية الانتقال إلى params و searchParams غير المتزامنة (تغيير جذري في Next.js 16)
  • كيفية التحقق من عمل البث في طبقة الشبكة باستخدام curl --no-buffer

المتطلبات الأساسية

  • Node.js 22 LTS (أسقطت Next.js 16 دعم Node 18؛ الحد الأدنى هو 20.9، ولكن 22 LTS هو الأساس الموصى به)3
  • pnpm 9+ أو npm 10+
  • طرفية (terminal) يتوفر بها curl لخطوة التحقق من البث
  • محرر أكواد مع خادم لغة TypeScript 5.7+ (TypeScript 6.0.3 هو الحالي؛ كلاهما سيعمل4)
  • إلمام أساسي بـ React Server Components — إذا كان أي من هذا جديداً عليك، فاقرأ إتقان أنماط Next.js App Router لتطبيقات الويب القابلة للتوسع أولاً

التبعيات المثبتة لهذا البرنامج التعليمي (لا توجد وسوم latest — تم توثيق انحراف الإصدارات في دليل الهجرة إلى Next.js 16 الخاص بهذا الموقع):

الحزمةالإصدارالسبب
next16.2.6أحدث إصدار مستقر اعتباراً من 12-05-20265
React19.2.6مطلوب بواسطة Next.js 166
React-dom19.2.6مطلوب بواسطة Next.js 16
@types/React19.2.14أنواع TS مطابقة لـ React 19.2
TypeScript5.7.3خط LTS المستقر؛ إصدار 6.0.x يعمل أيضاً

الخطوة 1 — إنشاء هيكل المشروع

pnpm dlx create-next-app@16.2.6 stream-cache-demo \
  --TypeScript --app --no-tailwind --no-eslint --no-src-dir \
  --import-alias '@/*' --use-pnpm
cd stream-cache-demo

تقوم هذه الأعلام (flags) بتثبيت الهيكل على إصدار Next.js 16.2.6، واختيار App Router، وتخطي Tailwind/ESLint لإبقاء البرنامج التعليمي مركزاً، وتعطيل تخطيط src/ بحيث تتطابق مسارات الملفات في هذا المنشور مع ما تراه على القرص.

تحقق من التثبيت:

pnpm next --version
# المتوقع: 16.2.6
node --version
# المتوقع: v22.x.y (أو v20.9.0+ إذا كنت تستخدم LTS القديم)

الخطوة 2 — تفعيل نموذج التخزين المؤقت الجديد

التخزين المؤقت في Next.js 16 هو اختياري افتراضياً. بدون علم cacheComponents، تعمل جميع مكونات الخادم الديناميكية في وقت الطلب ولا يتم تخزين أي شيء مؤقتاً ما لم تميزه صراحةً. قم بتفعيل الخيار في next.config.ts:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  // ملف تعريف مخصص لموجز البث الخاص بنا؛ يتواجد بجانب الملفات المدمجة مثل 'seconds' و 'minutes' و 'max'.
  cacheLife: {
    feed: { stale: 30, revalidate: 60, expire: 600 },
  },
};

export default nextConfig;

هناك شيئان يجب ملاحظتهما:

  1. cacheComponents: true هو الخلف المستقر الذي تم تغيير اسمه لـ experimental.dynamicIO من Next.js 15. إنه يتيح سلوكاً بأسلوب العرض المسبق الجزئي (Partial Prerendering): يتم بث هيكل ثابت فوراً، بينما يتم بث الأقسام الديناميكية بمجرد اكتمال بياناتها7.
  2. كائن تكوين cacheLife يتيح لك تحديد ملفات تعريف مسمى مخصصة. 'feed' هنا تعني: يمكن للعملاء استخدام القيمة المخزنة مؤقتاً لمدة 30 ثانية دون التحقق من الخادم، ويبدأ تحديث في الخلفية بعد 60 ثانية، ويتم طرد الإدخال غير المطلوب نهائياً بعد 10 دقائق. ملفات التعريف المسمى المدمجة (seconds، minutes، hours، days، weeks، max) متاحة دائماً بجانب ملفاتك المخصصة8.

الخطوة 3 — محاكاة قاعدة بيانات بطيئة

من أجل برنامج تعليمي قابل للتشغيل، لا نرغب في جلب Postgres، لذا سنقوم بمحاكاة التأخير باستخدام مخزن بيانات في الذاكرة. أنشئ lib/db.ts:

// lib/db.ts
import 'server-only';

export type Post = {
  id: string;
  title: string;
  author: string;
  body: string;
  createdAt: string;
};

const store: Post[] = [
  {
    id: '1',
    title: 'Streaming the App Router',
    author: 'Ada',
    body: 'Server Components stream HTML over chunked transfer encoding...',
    createdAt: '2026-05-10T10:00:00.000Z',
  },
  {
    id: '2',
    title: 'Why use cache is opt-in',
    author: 'Grace',
    body: 'Next.js 16 flipped the default: dynamic at request time unless you opt in.',
    createdAt: '2026-05-11T09:30:00.000Z',
  },
];

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function getPosts(): Promise<Post[]> {
  // محاكاة استعلام تحليلي بطيء
  await sleep(1200);
  return store.slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

export async function getStats(): Promise<{ totalPosts: number; uniqueAuthors: number > {
  // محاكاة تجميع سريع
  await sleep(200);
  const uniqueAuthors = new Set(store.map((p) => p.author)).size;
  return { totalPosts: store.length, uniqueAuthors ;
}

export async function createPost(input: Omit<Post, 'id' | 'createdAt'>): Promise<Post> {
  await sleep(150);
  const post: Post = {
    id: String(store.length + 1),
    ...input,
    createdAt: new Date().toISOString(),
  };
  store.unshift(post);
  return post;
}

سطر import 'server-only' هو اتفاقية في Next.js تطلق خطأ في وقت البناء إذا حاول مكون عميل (Client Component) سحب هذا الموديول — وهي شبكة أمان رخيصة للبرامج التعليمية وعادة ممتازة في كود الإنتاج.

الخطوة 4 — عرض الصفحة بدون تخزين مؤقت، بث فقط

هذا هو أبسط نمط للبث: مكون خادم غير متزامن واحد مع loading.tsx كبديل على مستوى المسار.

أنشئ app/page.tsx:

// app/page.tsx
import { Suspense } from 'React';
import { getPosts getStats } from '@/lib/db';

export const dynamic = 'force-dynamic'; // تأكد من أننا لا نقوم بالعرض المسبق بالخطأ

type PageProps = {
  searchParams: Promise<{ author?: string }>;
};

export default async function HomePage({ searchParams }: PageProps) {
  // Next.js 16: أصبح searchParams الآن Promise
  const { author } = await searchParams;

  return (
    <main style={{ maxWidth: 720 margin: '40px auto' fontFamily: 'system-ui' }}>
      <h1>المنشورات المباشرة {author ? `بواسطة ${author}` : ''}</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts authorFilter={author} />
      </Suspense>
    </main>
  );
}

async function Stats() {
  const { totalPosts uniqueAuthors } = await getStats();
  return (
    <p style={{ color: '#666' }}>
      <strong>{totalPosts}</strong> منشورات من <strong>{uniqueAuthors}</strong> مؤلفين.
    </p>
  );
}

async function Posts({ authorFilter }: { authorFilter?: string }) {
  const posts = await getPosts();
  const filtered = authorFilter
    ? posts.filter((p) => p.author.toLowerCase() === authorFilter.toLowerCase())
    : posts;
  return (
    <ul style={{ listStyle: 'none' padding: 0 }}>
      {filtered.map((p) => (
        <li key={p.id} style={{ borderTop: '1px solid #eee' padding: '12px 0' }}>
          <h3 style={{ margin: 0 }}>{p.title}</h3>
          <small>
            بواسطة {p.author} في {new Date(p.createdAt).toLocaleString()}
          </small>
          <p>{p.body}</p>
        </li>
      ))}
    </ul>
  );
}

function StatsSkeleton() {
  return <p style={{ color: '#aaa' }}>جاري تحميل الإحصائيات…</p>;
}

function PostsSkeleton() {
  return (
    <ul style={{ listStyle: 'none' padding: 0 }}>
      {[1 2 3].map((i) => (
        <li key={i} style={{ borderTop: '1px solid #eee' padding: '12px 0' }}>
          <div style={{ height: 16 width: '60%' background: '#eee' }} />
          <div style={{ height: 12 width: '30%' background: '#f3f3f3' margin: '6px 0' }} />
          <div style={{ height: 12 width: '90%' background: '#f3f3f3' }} />
        </li>
      ))}
    </ul>
  );
}

بعض الأشياء التي تستحق التوضيح:

  • params/searchParams غير المتزامنة. جعلت Next.js 16 كلاً من params و searchParams من نوع Promise. انتظارهم (await) إلزامي — استخدام searchParams.author مباشرةً يعد خطأ في وقت البناء ووقت التشغيل9. إذا كان لديك كود من إصدار Next.js 15، فقم بتشغيل npx @next/codemod@canary upgrade latest لأتمتة إعادة الكتابة10.
  • حدود Suspense متجاورة. تعود Stats في حوالي 200 مللي ثانية؛ بينما تستغرق Posts حوالي 1200 مللي ثانية. مع تغليف كليهما، تقوم الصفحة برسم العنوان فوراً، وتستبدل بديل الإحصائيات عند حوالي 200 مللي ثانية، وتستبدل بديل المنشورات عند حوالي 1200 مللي ثانية — بدون تسلسل انتظار (waterfall).
  • dynamic = 'force-dynamic'. إجراء احترازي لهذه الخطوة: سنقوم بإزالته بمجرد إضافة use cache حتى تتمكن من رؤية الفرق.

بديل اختياري على مستوى المسار. أنشئ app/loading.tsx:

// app/loading.tsx
export default function Loading() {
  return (
    <main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1 style={{ color: '#aaa' }}>جاري التحميل…</h1>
    </main>
  );
}

يقوم ملف loading.tsx تلقائيًا بتغليف page.tsx (وأي أجزاء مسار متداخلة تحته) في حدود <Suspense> تستخدم هذا الملف كحالة احتياطية (fallback)1. إنها الأداة المناسبة عندما تريد هيكلًا واحدًا على مستوى الصفحة ليتم بثه (stream) فورًا؛ يظل التخطيط (layout) الخاص بك (ملف app/layout.tsx) تفاعليًا بحيث يمكن للمستخدم الاستمرار في النقر على شريط التنقل أثناء تبديل محتوى الصفحة.

ابدأ خادم التطوير:

pnpm next dev

افتح http://localhost:3000. يجب أن ترى العنوان أولاً، ثم تظهر الإحصائيات بعد حوالي 200 مللي ثانية، وتظهر قائمة المنشورات بعد حوالي 1200 مللي ثانية. هذا هو البث (streaming) وهو يعمل.

كيف يمكنني بث مكونات الخادم في Next.js 16؟

قم بتغليف مكون خادم (Server Component) غير متزامن (async) في <Suspense fallback={...}> (أو ضع ملف loading.tsx بجوار ملف page.tsx الخاص بالمسار)، وسيقوم Next.js بإرسال الهيكل الثابت بالإضافة إلى الحالة الاحتياطية عبر ترميز نقل مجزأ (chunked transfer encoding) فورًا، ثم يبث كود HTML الخاص بالمكون غير المتزامن بمجرد اكتمال جلب بياناته2. لا يوجد وضع بث منفصل لتفعيله — فكل مكون خادم غير متزامن يشارك تلقائيًا طالما توجد حدود Suspense في مكان ما فوقه.

لإثبات أن الأجزاء (chunks) تصل بالفعل على مراحل، قم بتشغيل هذا الأمر ضد خادم التطوير الخاص بك:

curl --no-buffer -N http://localhost:3000 | head -c 4000

سترى كود HTML يُطبع في الجهاز الطرفي (terminal) مع توقفين مرئيين — الهيكل أولاً، ثم جزء Stats، ثم جزء Posts. كل جزء مغلف باستدعاءات <script>$RC(...)</script> التي يستخدمها React لتبديل المحتوى المبثوث في مكان Suspense الصحيح3.

متى يجب استخدام loading.tsx مقابل Suspense في Next.js؟

استخدم loading.tsx عندما يكون المسار بالكامل غير متزامن وتريد هيكلًا واحدًا على مستوى الصفحة يبث فورًا مع الحفاظ على تفاعلية التخطيط؛ استخدم <Suspense> عندما يؤدي اعتماد واحد بطيء للبيانات إلى حجب عناصر أخرى أسرع وتريد بثًا تدريجيًا داخل الصفحة. إنهما ليسا حصريين — يوفر loading.tsx الحدود الخارجية، ويوفر <Suspense> الحدود الداخلية.

القاعدة العملية:

النمطمتى تختاره
المسار بالكامل بطيء؛ حالة احتياطية واحدة تكفيloading.tsx
الصفحة تحتوي على إحصائية سريعة + قائمة بطيئةاستخدام <Suspense> متجاور داخل page.tsx
التخطيط (Layout) يقرأ بيانات وقت التشغيلغلف قراءة وقت التشغيل في التخطيط بـ <Suspense> (ملف loading.tsx لن يغطي التخطيطات التي تنتظر بيانات وقت التشغيل بنفسها)
واجهة مستخدم متفائلة (Optimistic UI) في نموذجاستخدام <Suspense> مع useTransition معًا

الخطوة 5 — تخزين الاستعلام البطيء مؤقتًا باستخدام 'use cache'

الآن الجزء الجديد. المنشورات لا تحتاج حقًا إلى إعادة التحقق في كل طلب — لنقم بتقديم نسخة مخزنة مؤقتًا صالحة لمدة 60 ثانية يتم تحديثها في الخلفية. قم بتعديل lib/db.ts وأضف نسخة مخزنة من getPosts:

// lib/db.ts (إضافات)
import { cacheLife } from 'next/cache';
import { cacheTag } from 'next/cache';

export async function getCachedPosts(): Promise<Post[]> {
  'use cache';
  cacheLife('feed'); // يطابق الملف الشخصي في next.config.ts
  cacheTag('posts'); // تسمية يمكننا إبطالها لاحقًا
  return getPosts();
}

تحدث ثلاثة أشياء هنا:

  1. 'use cache' يحدد القيمة الراجعة للدالة كقابلة للتخزين المؤقت. يقوم مترجم Next.js تلقائيًا بإنشاء مفتاح تخزين مؤقت من وسيطات الدالة — لا يلزم وجود وسيط keyParts4.
  2. cacheLife('feed') يطبق الملف الشخصي الذي حددناه في next.config.ts: نافذة بيانات قديمة (stale) لمدة 30 ثانية، إعادة تحقق في الخلفية لمدة 60 ثانية، وانتهاء صلاحية نهائي بعد 10 دقائق. يمكنك أيضًا تمرير اسم ملف شخصي مدمج ('minutes'، 'hours'، 'max') أو كائن مباشر مثل { stale: 60, revalidate: 300, expire: 3600 }5.
  3. cacheTag('posts') يضع علامة (tag) على إدخال التخزين المؤقت هذا. سنستخدمها من إجراء خادم (Server Action) في الخطوة 7 للإبطال بشكل انتقائي.

استبدل الاستدعاء داخل app/page.tsx:

// app/page.tsx (استبدل مكون Posts)
import { getCachedPosts } from '@/lib/db';

async function Posts({ authorFilter }: { authorFilter?: string }) {
const posts = await getCachedPosts();
const filtered = authorFilter
? posts.filter((p) => p.author.toLowerCase() === authorFilter.toLowerCase())
: posts;
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filtered.map((p) => (
<li key={p.id} style={{ borderTop: '1px solid #eee', padding: '12px 0' }}>
<h3 style={{ margin: 0 }}>{p.title}</h3>
<small>
بواسطة {p.author} في {new Date(p.createdAt).toLocaleString()}
</small>
<p>{p.body}</p>
</li>
))}
</ul>
  );
}

يمكنك الآن إزالة export const dynamic = 'force-dynamic' — فتوجيه 'use cache' هو ما يتحكم في ما إذا كان هذا الفرع سيُخزن مؤقتًا أم لا، وليس العلامة على مستوى الصفحة. هذه هي الخطوة التي تحدد نموذج البث الحديث في Next.js 16: يبث الهيكل الثابت فورًا، ويبث قسم المنشورات المخزنة مؤقتًا من التخزين المؤقت عندما يكون جاهزًا، وفقط الأجزاء غير المخزنة مؤقتًا هي التي تدفع تكلفة جلب البيانات في كل طلب.

أعد تحميل http://localhost:3000 مرتين. يستغرق التحميل الأول حوالي 1200 مللي ثانية لقائمة المنشورات (التخزين المؤقت فارغ). التحميل الثاني يعيد البيانات المخزنة مؤقتًا في أجزاء من الثانية. قم بالتحديث مرة أخرى عند علامة 30 ثانية وسترى القيمة القديمة تُقدم فورًا بينما يحدث تحديث في الخلفية.

ماذا يفعل توجيه use cache في Next.js 16؟

يحدد توجيه 'use cache' دالة أو مكونًا أو ملفًا بالكامل كقابل للتخزين المؤقت؛ يقوم Next.js بإنشاء مفتاح تخزين مؤقت من وسيطات الدالة وأي قيم ملتقطة (closure-captured)، ويخزن المخرجات المعالجة في ذاكرة تخزين البيانات والمسار الكامل، ويقدم القيمة المخزنة (وفقًا لملف cacheLife النشط) في الطلبات اللاحقة عبر المجموعة (cluster)6. إنه يحل محل كل من تخزين fetch الضمني ومساعد unstable_cache القديم من عصر Next.js 15 — وبخلاف هؤلاء، فهو مدرك للمترجم، لذا لا تحتاج لتمرير مصفوفة keyParts يدويًا.

جدول قواعد إرشادية قصير:

النطاقأين تضع 'use cache'
تخزين القيمة الراجعة لدالة واحدةأول سطر داخل جسم الدالة
تخزين جميع صادرات وحدة (module) بالكاملأول سطر في أعلى الملف
تخزين مكون خادم واحدأول سطر داخل جسم المكون
عدم تخزين أي شيء في هذا الفرعاحذف التوجيه — يكون وقت الطلب افتراضيًا

الخطوة 6 — تكوين cacheTag لتتمكن من الإبطال لاحقًا

cacheTag هو الفعل؛ والوسم (tag) هو الاسم. يمكنك إرفاق وسوم متعددة بإدخال تخزين مؤقت واحد — وهو أمر مفيد عندما يجب إبطال نفس الإدخال إما عن طريق حدث على مستوى المؤلف ("نشرت Ada منشورًا") أو حدث عام ("تم إعادة بناء كتالوج المنشورات"):

// lib/db.ts (تنويع)
export async function getCachedPostsByAuthor(author: string): Promise<Post[]> {
  'use cache';
  cacheLife('feed');
  cacheTag('posts');
  cacheTag(`posts:author:${author}`);
  const all = await getPosts();
  return all.filter((p) => p.author === author);
}

يستجيب نفس إدخال التخزين المؤقت الآن للإبطال بواسطة 'posts' (الكل) أو 'posts:author:Ada' (جزء Ada فقط). دقة التفاصيل تعود إليك؛ القاعدة العامة هي وسم عالمي واحد بالإضافة إلى وسم أو وسمين محددين لكل دالة مخزنة مؤقتًا.

الخطوة 7 — التعديل، ثم الإبطال باستخدام updateTag

حان الوقت للجزء الذي تتجاهله معظم دروس البث التعليمية. يقوم المستخدم بإنشاء منشور عبر إجراء خادم (Server Action)؛ ومباشرة بعد عودة الإجراء، سينتقل المستخدم مرة أخرى إلى الصفحة الرئيسية متوقعًا رؤية منشوره الجديد. مع revalidateTag، يتم إبطال التخزين المؤقت مع دلالات "القديم أثناء إعادة التحقق" (stale-while-revalidate) — مما يعني أن المستخدم قد لا يزال يرى القائمة القديمة في العرض التالي. مع updateTag، تنتهي صلاحية التخزين المؤقت ويتم تحديثه ضمن نفس الطلب، لذا ينتهي التنقل بعرض جديد تمامًا. هذا هو ضمان "قراءة ما كتبته" (read-your-writes)7.

قم بإنشاء app/new/page.tsx:

// app/new/page.tsx
import { redirect } from 'next/navigation';
import { updateTag } from 'next/cache';
import { createPost } from '@/lib/db';

async function createPostAction(formData: FormData) {
  'use server';
const title = String(formData.get('title') ?? '').trim();
const author = String(formData.get('author') ?? '').trim();
const body = String(formData.get('body') ?? '').trim();
  if (!title || !author || !body) {
    throw new Error('All fields are required');
  }
  await createPost({ title, author, body });
  // Read-your-writes: expire AND refresh within this request
  updateTag('posts');
  redirect('/');
}

export default function NewPostPage() {
  return (
    <main style={{ maxWidth: 720, margin: '40px auto', fontFamily: 'system-ui' }}>
      <h1>New post</h1>
      <form action={createPostAction} style={{ display: 'grid', gap: 12 }}>
        <label>
          Title <input name="title" required style={{ width: '100%' }} />
        </label>
        <label>
          Author <input name="author" required style={{ width: '100%' }} />
        </label>
        <label>
          Body <textarea name="body" required rows={5} style={{ width: '100%' }} />
        </label>
        <button type="submit">Publish</button>
      </form>
    </main>
  );
}

أرسل النموذج. يتم تشغيل الإجراء (action) من جانب الخادم، ويقوم بتعديل المخزن في الذاكرة، ويستدعي updateTag('posts') لإنهاء صلاحية المدخلات الموسومة بـ 'posts'، ثم يعيد التوجيه إلى /. يتم إعادة رندر مكون Posts في الصفحة الرئيسية بالقائمة المحدثة فوراً — دون ظهور بيانات قديمة (stale flash).

ما الفرق بين updateTag و revalidateTag في Next.js 16؟

كلاً من updateTag و revalidateTag يقومان بإبطال مدخلات التخزين المؤقت (cache) حسب الوسم (tag)، لكن updateTag يقوم بذلك مع دلالات "اقرأ ما كتبت" (إنهاء الصلاحية والتحديث ضمن نفس الطلب، وتقديم بيانات طازجة في الرندر التالي) بينما يستخدم revalidateTag دلالات "تقديم القديم أثناء التحديث" (تقديم القيمة القديمة فوراً، والتحديث في الخلفية)18. استخدم updateTag بعد عملية تعديل بدأها المستخدم عندما سينتقل المستخدم إلى صفحة يجب أن تظهر فيها التغييرات؛ واستخدم revalidateTag من الـ webhooks أو المهام الخلفية عندما يكون التأخير الطفيف مقبولاً.

قيدان رئيسيان يستحقان الحفظ:

  • updateTag مخصص لـ Server-Action فقط. سيؤدي استدعاؤه من Route Handler أو Client Component أو middleware إلى حدوث خطأ.
  • revalidateTag الآن يأخذ ملف تعريف cacheLife كوسيط ثانٍ: revalidateTag('posts', 'max'). الافتراضي الموصى به هو 'max' (أطول عمر لـ SWR)، إلا إذا كنت بحاجة إلى إنهاء صلاحية فوري وصارم ({ expire: 0 } كوسيط ثانٍ)19.
حالة الاستخدامماذا تختار
المستخدم يرسل نموذجاً؛ وسيرى النتيجةupdateTag داخل الـ Server Action
انطلاق Webhook من نظام إدارة محتوى (CMS)revalidateTag('cms', 'max') في route handler
الحاجة إلى أن ينتظر الزائر التالي البيانات الطازجةrevalidateTag('cms', { expire: 0 })
مهمة مجدولة (Cron job) تعيد بناء تجميعة بطيئةrevalidateTag('aggregate', 'max')

الخطوة 8 — التحقق من البث (streaming) على مستوى الشبكة

غالباً ما تعرض لوحة الشبكة في أدوات مطوري المتصفح الطلب كاستجابة واحدة — الأجزاء (chunks) حقيقية لكن واجهة المستخدم تخفيها. استخدم curl --no-buffer للتأكيد:

curl --no-buffer -N -s http://localhost:3000 \
  | tr -d '\n' \
  | tr '>' '>\n' \
  | grep -nE 'Loading stats|Loading posts|by Ada|by Grace' \
  | head -20

قم بتشغيل هذا الأمر وراقب النتائج وهي تتدفق في التيرمينال. يجب أن ترى "Loading stats" و "Loading posts" تصل أولاً (الـ fallbacks)، تليها الأسطر التي تشير إلى المحتوى الذي تم بثه. إذا تمت طباعة المخرجات بالكامل مرة واحدة دون تأخير مرئي، فإن البث لا يعمل — السبب الأرجح هو وجود بروكسي أو middleware للضغط يقوم بتخزين الاستجابة مؤقتاً20.

يمكنك أيضاً فحص الأدلة على مستوى HTTP:

curl -sI http://localhost:3000 | grep -i 'transfer-encoding\|content-encoding'
# المتوقع:
# Transfer-Encoding: chunked

chunked هو الدليل القاطع — لا يوجد Content-Length لأن طول جسم الاستجابة غير معروف مسبقاً، وهذا ما يسمح بالإرسال التدريجي (progressive flushing).

الخطوة 9 — خطأ شائع: لا تقرأ بيانات وقت التشغيل (runtime data) في تخطيط (layout) يغلف صفحات مخزنة مؤقتاً

إذا كان ملف app/layout.tsx يقرأ أي شيء ديناميكي (ملفات تعريف الارتباط، الرؤوس، عنوان URL للطلب) ولم يكن هو نفسه مغلفاً بحدود <Suspense>، فإن المسار بالكامل تحته يجب أن ينتظر تلك القراءة قبل أن يتمكن الهيكل (shell) من الإرسال. الحل هو دفع قراءة وقت التشغيل إلى مكون عميل (client component) خاص بها، أو تغليف جزء صغير من الخادم في <Suspense> حتى يتمكن باقي التخطيط من الإرسال فوراً21.

النمط:

// app/layout.tsx (مصحح)
import { Suspense } from 'React';
import { CurrentUser } from './_components/current-user';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>
          <a href="/">Home</a> · <a href="/new">New post</a> ·
          <Suspense fallback={<span style={{ color: '#999' }}></span>}>
            <CurrentUser />
          </Suspense>
        </nav>
        {children}
      </body>
    </html>
  );
}

يمكن لـ CurrentUser الآن القيام بـ await cookies() دون منع بقية الصفحة من البث.

التحقق

قم بالمرور على قائمة التحقق الكاملة:

# 1. تأكد من تشغيل خادم التطوير على Next.js 16.2.6
pnpm next --version
# المتوقع: 16.2.6

# 2. تأكد من تفعيل chunked transfer encoding
curl -sI http://localhost:3000 | grep -i transfer-encoding
# المتوقع: Transfer-Encoding: chunked

# 3. تأكد من أن البث يبث فعلياً (سترى توقفين مرئيين)
time curl --no-buffer -N -s http://localhost:3000 > /dev/null
# المتوقع: الوقت الحقيقي بين 1.2 ثانية و 1.5 ثانية

# 4. تأكد من عمل التخزين المؤقت في التحميل الثاني (أقل من 200 مللي ثانية)
time curl -s http://localhost:3000 > /dev/null
# المتوقع: الوقت الحقيقي أقل من 200 مللي ثانية

# 5. أرسل منشوراً جديداً وتأكد من ظهوره فوراً
curl -s -X POST -F 'title=Test' -F 'author=Tutorial' -F 'body=Hello' \
  http://localhost:3000/new -i | head -3
# المتوقع: HTTP/1.1 303 See Other مع Location: /

إذا نجحت الاختبارات الخمسة، فقد قمت بشحن خط أنابيب بث + تخزين مؤقت صحيح من حدود الطلب وصولاً إلى إبطال "اقرأ ما كتبت".

الأخطاء الشائعة

Type error: Property 'author' does not exist on type 'Promise<...>'. لقد نسيت استخدام await مع params أو searchParams. في Next.js 16 أصبحوا وعوداً (Promises)؛ الـ codemod (npx @next/codemod@canary upgrade latest) يصلح معظم أماكن الاستدعاء تلقائياً10.

use cache is not allowed inside a synchronous function. الدالة التي تحتوي على 'use cache' يجب أن تكون async. تنطبق التوجيهات على القيمة الراجعة من دالة غير متزامنة، وليس على مساعد متزامن. قم بتحويل توقيع الدالة إلى async وسيختفي الخطأ.

cacheTag was called outside of a "use cache" scope. يجب استدعاء كل من cacheTag و cacheLife داخل دالة (أو ملف) يكون أول بيان فيها هو توجيه 'use cache'. إذا رأيت هذا الخطأ، فقم بنقل الاستدعاء أسفل التوجيه، أو ارفع المنطق بالكامل إلى دالة 'use cache'.

updateTag is only available inside Server Actions. لقد استدعيت updateTag من Route Handler أو Client Component أو في مكان ما خارج حدود 'use server'. انقل الاستدعاء إلى Server Action؛ إذا كنت بحاجة فعلاً للإبطال من webhook، استخدم revalidateTag(tag, 'max') بدلاً من ذلك.

البث "يعمل" في بيئة التطوير ولكن ليس في الإنتاج. تحقق من البروكسي العكسي (reverse proxy). سيقوم Nginx الذي تمت تهيئته بشكل خاطئ (proxy_buffering on، وهو الافتراضي) بتخزين الاستجابة بالكامل وكسر البث المجزأ (chunked streaming) من البداية للنهاية. أضف proxy_buffering off لمسار Next.js، أو اختبر مباشرة ضد node/pnpm next start للتأكد من أن الإطار يرسل الأجزاء قبل إلقاء اللوم على Next.js22.

الخطوات التالية

لديك الآن بث، تخزين مؤقت اختياري، وإبطال "اقرأ ما كتبت" في صفحة واحدة. من هنا:

  • قم بتوصيل قاعدة بيانات حقيقية — بالنسبة لـ Postgres، ابدأ بـ Production Postgres pooling with PgBouncer & Prisma Supavisor
  • أضف Server Actions لكل صف تستدعي updateTag(posts:author:${author}) لإبطال دقيق
  • اجمع بين useTransition و <Suspense> لواجهة مستخدم متفائلة (optimistic UI) غير مانعة في نموذج المنشور الجديد23
  • انقل المخزن في الذاكرة خلف مثيل Redis وأعد استخدام نفس 'use cache' API — التوجيه لا يهتم بمصدر البيانات الأساسي، فقط بوسائط الدالة

إذا كنت لا تزال في مرحلة الانتقال من Pages Router، فإن Next.js Pages Router to App Router migration tutorial يغطي المعاملات غير المتزامنة، والـ codemods، وفخاخ التخطيط التي تظهر أثناء الانتقال. للحصول على النموذج الذهني الأوسع لـ RSC — ما الذي تترجم إليه Server Components فعلياً ولماذا تشحن صفر جافا سكريبت — راجع React Server Components: the future of seamless rendering.


Footnotes

  1. Next.js، "الأدلة: البث (Streaming)." https://nextjs.org/docs/app/guides/streaming

  2. Next.js، "Next.js 16" (منشور مدونة الإصدار، 2025-10-22). https://nextjs.org/blog/next-16

  3. Next.js، "الترقية: الإصدار 16." https://nextjs.org/docs/app/guides/upgrading/version-16

  4. إصدارات TypeScript على npm — npm view TypeScript version أرجعت 6.0.3 (نُشرت في 2026-04-16) وقت الكتابة؛ الإصدار 5.7.x لا يزال مدعومًا. https://www.npmjs.com/package/TypeScript

  5. npm view next time --json16.2.6 نُشرت في 2026-05-07T19:01:54.751Z. https://www.npmjs.com/package/next

  6. Next.js، "الحد الأدنى لإصدار React." https://nextjs.org/docs/messages/React-version

  7. Next.js، "next.config.js: cacheComponents." https://nextjs.org/docs/app/API-reference/config/next-config-js/cacheComponents

  8. Next.js، "الوظائف: cacheLife." https://nextjs.org/docs/app/API-reference/functions/cacheLife

  9. Next.js، "واجهات البرمجة الديناميكية غير متزامنة (Asynchronous)." https://nextjs.org/docs/messages/sync-dynamic-apis

  10. Next.js، "الترقية: الإصدار 16 — الترقية الآلية." https://nextjs.org/docs/app/guides/upgrading/version-16 2

  11. Next.js، "اتفاقيات نظام الملفات: loading.js." https://nextjs.org/docs/app/API-reference/file-conventions/loading

  12. Next.js، "App Router: البث (Streaming)." https://nextjs.org/learn/dashboard-app/streaming

  13. React، "Suspense — مرجع." https://React.dev/reference/React/Suspense

  14. Next.js، "التوجيهات: use cache." https://nextjs.org/docs/app/API-reference/directives/use-cache

  15. Next.js، "الوظائف: cacheLife — الملفات الشخصية الافتراضية." https://nextjs.org/docs/app/API-reference/functions/cacheLife

  16. منشور مدونة Next.js 16.2 — "use cache أصبح مستقرًا الآن." https://nextjs.org/blog/next-16-2

  17. Next.js، "الوظائف: updateTag." https://nextjs.org/docs/app/API-reference/functions/updateTag

  18. Next.js، "الوظائف: revalidateTag." https://nextjs.org/docs/app/API-reference/functions/revalidateTag

  19. Next.js، "البداية: إعادة التحقق (Revalidating)." https://nextjs.org/docs/app/getting-started/revalidating

  20. Next.js، "الأدلة: البث — تنبيهات تخزين الوكيل المؤقت (Proxy buffering)." https://nextjs.org/docs/app/guides/streaming

  21. Next.js، "البداية: مكونات الخادم والعميل (Server and Client Components)." https://nextjs.org/docs/app/getting-started/server-and-client-components

  22. مناقشة Next.js على GitHub رقم 58865 — "Suspense And Loading" (سلسلة نقاش الوكيل/التخزين المؤقت). https://GitHub.com/vercel/Next.js/discussions/58865

  23. React، "useTransition — مرجع." https://React.dev/reference/React/useTransition


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.