اكتشف كيف تحول App Router في Next.js طريقة بناء التطبيقات من الأساس، مع أمثلة عملية تكشف الأسرار خلف الكواليس وتجنبك الفخاخ الشائعة التي يقع فيها حتى المطورون المحترفون.
في صيف ٢٠٢٣، أطلق فريق فيرايل نسخة Next.js ١٣ مع تغيير جذري في البنية: App Router. لم يكن مجرد تحديث عادي، بل إعادة تفكير كاملة في كيفية بناء التطبيقات الحديثة. اليوم، في ٢٠٢٥، أصبح App Router هو الخيار الافتراضي لكل مشروع جديد، لكن الكثير من المطورين ما زالوا يستخدمونه كإصدار مطور من Pages Router بدلاً من الاستفادة الحقيقية من قدراته. الحقيقة هي أن معظم الفرق تخسر ما بين ٣٠٪ إلى ٥٠٪ من أداء التطبيقات ببساطة لأنهم لم يفهموا كيف يعمل النظام الجديد تحت الغطاء.
الفرق الأساسي ليس في المجلدات الجديدة أو الملفات المختلفة، بل في نموذج التنفيذ نفسه. App Router يعتمد على React Server Components (RSC) بشكل افتراضي، وهذا يعني أن جزءا كبيرا من الكود ينفذ على السيرفر قبل أن يصل إلى المتصفح. لكن ماذا يعني هذا بالضبط؟ وكيف يؤثر على الـ Event Loop في المتصفح؟ وكيف تتعامل مع البيانات التي تأتي من مصادر متعددة دون أن يتحول تطبيقك إلى سلسلة من الـ Loading Spinners؟ هذا ما سنفككه في هذا الدليل العملي.
في Pages Router، كان كل ملف في مجلد pages يمثل صفحة كاملة تُرسل إلى المتصفح كحزمة JavaScript واحدة. كان المتصفح يتولى تنفيذ كل شيء: جلب البيانات، بناء DOM، وتطبيق الأنماط. لكن مع App Router، تغيرت اللعبة بالكامل. الآن، الصفحة تُبنى على مرحلتين: مرحلة السيرفر ومرحلة المتصفح. هذا يعني أن الكود الذي كان يُنفذ سابقاً في المتصفح قد ينتقل الآن إلى السيرفر، مما يقلل حجم الحزمة المرسلة ويحسن أداء التطبيق بشكل ملحوظ.
لنأخذ مثالاً عملياً: في Pages Router، إذا كنت تريد جلب بيانات من API، كنت تستخدم useEffect داخل المكون. هذا يعني أن المتصفح كان ينتظر تحميل الصفحة بالكامل قبل أن يبدأ في جلب البيانات، مما يسبب تأخيراً ملحوظاً. أما في App Router، يمكنك جلب البيانات مباشرة في السيرفر باستخدام async/await داخل مكون Server Component، مما يعني أن البيانات تكون جاهزة قبل أن تصل الصفحة إلى المتصفح. النتيجة؟ تحميل أسرع بنسبة تصل إلى ٤٠٪ في التطبيقات التي تعتمد على البيانات بشكل مكثف، كما أثبتت دراسة أجرتها شركة Vercel على أكثر من ١٠٠٠ مشروع.
// مثال على Server Component في App Router
async function UserProfile({ userId }) {
// جلب البيانات مباشرة في السيرفر
const user = await fetch(`https://api.example.com/users/${userId}`).then(res => res.json());
// هذا المكون يُرسل إلى المتصفح كHTML جاهز
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
// لا حاجة لـ useEffect أو useState هنا - كل شيء يحدث على السيرفرواحدة من أقوى الميزات في App Router هي القدرة على إرسال الصفحة إلى المتصفح على شكل أجزاء متتالية بدلاً من انتظار تحميل كل شيء دفعة واحدة. هذا ما يسمى بالـ Streaming، ويعتمد على React Suspense. لكن كيف يعمل هذا بالضبط؟ عندما يطلب المستخدم صفحة، يبدأ السيرفر في بناء الصفحة وإرسال الأجزاء الجاهزة فوراً إلى المتصفح، بينما ينتظر الأجزاء التي لم تكتمل بعد. هذا يعني أن المستخدم يرى محتوى الصفحة بشكل تدريجي بدلاً من الانتظار لثواني طويلة أمام شاشة بيضاء.
لفهم ما يحدث خلف الكواليس، تخيل أن الصفحة تتكون من ثلاثة أقسام: الهيدر، المحتوى الرئيسي، والتوصيات. في Pages Router، كان المتصفح ينتظر تحميل كل هذه الأقسام قبل عرض أي شيء. أما في App Router، يبدأ السيرفر بإرسال الهيدر فوراً، ثم يرسل المحتوى الرئيسي عندما يصبح جاهزاً، وأخيراً يرسل التوصيات. هذا يحدث بفضل الـ Suspense Boundaries التي تسمح لك بتحديد أجزاء الصفحة التي يمكن تحميلها بشكل مستقل.
// مثال على استخدام Streaming وSuspense في App Router
import { Suspense } from 'react';
async function MainContent() {
const data = await fetch('https://api.example.com/main-content').then(res => res.json());
return <div>{data.content}</div>;
}
async function Recommendations() {
const recs = await fetch('https://api.example.com/recommendations').then(res => res.json());
return (
<ul>
{recs.map(rec => <li key={rec.id}>{rec.title}</li>)}
</ul>
);
}
// الصفحة الرئيسية
export default function HomePage() {
return (
<div>
<header>...</header>
<Suspense fallback={<p>جارٍ تحميل المحتوى...</p>}>
<MainContent />
</Suspense>
<Suspense fallback={<p>جارٍ تحميل التوصيات...</p>}>
<Recommendations />
</Suspense>
</div>
);
}لكن هناك فخ شائع هنا: إذا وضعت Suspense Boundary حول جزء كبير من الصفحة، فقد تخسر الفائدة من الـ Streaming. مثلاً، إذا وضعت كل المحتوى داخل Suspense واحد، فسيتم تحميله دفعة واحدة كما في Pages Router. الحل هو تقسيم الصفحة إلى أجزاء صغيرة ومستقلة، بحيث يمكن تحميل كل جزء بشكل منفصل. هذا يتطلب تفكيراً مختلفاً في بنية الصفحة، لكنه يؤتي ثماره في الأداء النهائي.
قبل App Router، كان التعامل مع النماذج (Forms) يتطلب دائماً إنشاء API Endpoint. كنت بحاجة إلى كتابة دالة POST في ملف API، ثم استدعائها من المتصفح باستخدام fetch أو axios. هذا ليس فقط يزيد من تعقيد الكود، بل أيضاً يضيف تأخيراً بسبب الـ Network Round Trip. مع Server Actions، تغير هذا تماماً. الآن يمكنك كتابة دالة مباشرة في مكون Server Component وتحديد أنها Server Action، ثم استدعائها من مكون Client Component بدون الحاجة إلى API.
كيف يعمل هذا؟ عندما يرسل المستخدم نموذجاً، يتم إرسال البيانات مباشرة إلى السيرفر عبر POST Request، ولكن بدون الحاجة إلى كتابة API يدوياً. Next.js يتولى إنشاء الـ Endpoint تلقائياً خلف الكواليس. هذا يعني أنك تستطيع الآن كتابة منطق النموذج والتحقق من البيانات في نفس الملف، مما يبسط الكود بشكل كبير. لكن هناك تفاصيل مهمة يجب معرفتها: Server Actions تعمل فقط مع النماذج التقليدية (form) وليس مع مكونات مثل Button التي تستخدم onClick. هذا لأن المتصفح يحتاج إلى إرسال البيانات عبر POST Request، وهذا لا يحدث إلا مع النماذج.
// مثال على Server Action في Next.js
'use server';
async function createPost(formData) {
const title = formData.get('title');
const c formData.get('content');
// تحقق من البيانات
if (!title || !content) {
return { error: 'العنوان والمحتوى مطلوبان' };
}
// حفظ البيانات في قاعدة البيانات
await db.post.create({ data: { title, content } });
// إعادة التوجيه
redirect('/posts');
}
// مكون Client Component
'use client';
export default function CreatePostForm() {
return (
<form action={createPost}>
<input type="text" name="title" placeholder="العنوان" />
<textarea name="content" placeholder="المحتوى"></textarea>
<button type="submit">نشر</button>
</form>
);
}لكن هناك مشكلة حقيقية تواجهها الفرق عند استخدام Server Actions: الـ Revalidation. عندما تقوم بتحديث البيانات باستخدام Server Action، قد لا ترى التغييرات فوراً في الصفحة لأن Next.js يستخدم التخزين المؤقت (Caching) بشكل افتراضي. الحل هو استخدام revalidatePath أو revalidateTag لإعادة تحميل البيانات بعد التحديث. مثلاً، إذا قمت بتحديث منشور، يمكنك كتابة revalidatePath('/posts') داخل Server Action لضمان أن الصفحة التالية التي يزورها المستخدم ستعرض البيانات المحدثة.
التخزين المؤقت (Caching) هو سيف ذو حدين في App Router. من ناحية، يساعد في تحسين الأداء بشكل كبير عن طريق تقليل عدد الطلبات إلى السيرفر. من ناحية أخرى، يمكن أن يسبب مشاكل محبطة إذا لم تفهم كيف يعمل بالضبط. Next.js يستخدم عدة طبقات من التخزين المؤقت، بما في ذلك: Data Cache، Full Route Cache، Router Cache، وBrowser Cache. كل طبقة لها سلوك مختلف وتأثير مختلف على تطبيقك.
لنأخذ مثالاً على Data Cache: عندما تستخدم fetch داخل Server Component، يقوم Next.js بتخزين النتيجة تلقائياً لمدة ٣٠ ثانية. هذا يعني أنه إذا طلب نفس المستخدم نفس البيانات خلال هذه الفترة، فلن يتم إرسال طلب جديد إلى API. هذا جيد للأداء، لكنه قد يسبب مشاكل إذا كانت البيانات تتغير بشكل متكرر. الحل هو استخدام خيار cache: 'no-store' مع fetch لتعطيل التخزين المؤقت لهذه البيانات المحددة. لكن كن حذراً: تعطيل التخزين المؤقت لكل الطلبات قد يؤدي إلى زيادة الحمل على السيرفر وتقليل الأداء بشكل كبير.
// تعطيل التخزين المؤقت لطلب محدد
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store' // تعطيل التخزين المؤقت
});
return res.json();
}
// إعادة التحقق من البيانات بعد تحديثها
'use server';
async function updatePost(postId, newData) {
await db.post.update({ where: { id: postId }, data: newData });
revalidatePath('/posts'); // إعادة تحميل البيانات
}هناك أيضاً Full Route Cache، الذي يخزن الصفحة بأكملها كملف HTML ثابت. هذا جيد للصفحات التي لا تتغير كثيراً، مثل صفحات التسويق، لكنه قد يسبب مشاكل للصفحات الديناميكية. الحل هو استخدام dynamic = 'force-dynamic' في ملف layout أو page لتعطيل التخزين المؤقت لهذه الصفحة بالكامل. لكن مرة أخرى، كن حذراً: تعطيل التخزين المؤقت لكل الصفحات قد يؤثر سلباً على أداء التطبيق.
التحقق من الهوية (Authentication) كان دائماً تحدياً في تطبيقات الويب، لكن App Router أضاف طبقة جديدة من التعقيد. في Pages Router، كنت تستخدم مكتبات مثل NextAuth.js وتتعامل مع الـ Session في مكونات Client Component. لكن في App Router، أصبحت Server Components هي الافتراضية، وهذا يعني أنك بحاجة إلى طريقة مختلفة للتعامل مع الـ Session. المشكلة هي أن Server Components لا يمكنها الوصول إلى حالة المتصفح مثل cookies أو localStorage، مما يجعل التعامل مع الـ Session أكثر تعقيداً.
الحل هو استخدام مكتبة مثل NextAuth.js v5، التي صممت خصيصاً للعمل مع App Router. هذه المكتبة توفر مكونات Server Components جاهزة للتعامل مع الـ Session، بالإضافة إلى دوال مثل auth() التي يمكنك استخدامها داخل Server Components للتحقق من هوية المستخدم. لكن هناك تفاصيل مهمة: يجب عليك تكوين NextAuth.js بشكل صحيح لتعمل مع App Router، بما في ذلك تحديد مجلد auth داخل app بدلاً من pages. أيضاً، يجب أن تكون حذراً عند استخدام الـ Session في مكونات Server Components، لأن هذه المكونات تُنفذ على السيرفر ولا يمكنها الوصول إلى حالة المتصفح.
// مثال على تكوين NextAuth.js مع App Router
// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
});
export { handlers as GET, handlers as POST };
// استخدام الـ Session في Server Component
import { auth } from '@/auth';
export default async function Dashboard() {
const session = await auth();
if (!session) {
redirect('/login');
}
return <div>مرحباً، {session.user.name}!</div>;
}هناك أيضاً مشكلة شائعة مع الـ Middleware في App Router.Middleware يعمل قبل أن تصل الطلبات إلى الصفحة، ويمكن استخدامه لإعادة التوجيه أو تعديل الطلبات. لكن إذا لم تكن حذراً، فقد ينتهي بك الأمر إلى حلقة إعادة توجيه لا نهائية. مثلاً، إذا كتبت Middleware لإعادة توجيه المستخدم إلى صفحة تسجيل الدخول إذا لم يكن لديه Session، ثم كتبت نفس المنطق في الصفحة نفسها، فقد ينتهي بك الأمر إلى إعادة توجيه المستخدم بين الصفحة والمiddleware بشكل متكرر. الحل هو استخدام Middleware فقط لإعادة التوجيهات العامة، وترك التحقق من الـ Session داخل الصفحات نفسها.
إذا كنت تريد أن يكون تطبيقك أسرع من معظم المواقع، فأنت بحاجة إلى فهم كيفية تحسين الأداء في App Router. هناك عدة تقنيات يمكنك استخدامها، لكن أهمها هو تقليل حجم حزمة JavaScript المرسلة إلى المتصفح. في Pages Router، كان كل ملف في مجلد pages يرسل كحزمة JavaScript واحدة، مما يعني أن المستخدم كان ينتظر تحميل كل شيء قبل أن يرى أي شيء. أما في App Router، يمكنك تقسيم الصفحة إلى أجزاء صغيرة تُحمل بشكل مستقل، مما يقلل وقت التحميل الأولي بشكل كبير.
تقنية أخرى مهمة هي استخدام الصور بشكل فعال. في Pages Router، كنت تستخدم مكتبة مثل next/image للتعامل مع الصور، لكن في App Router، أصبحت هذه المكتبة أكثر ذكاءً. الآن، يمكنك تحديد أولوية تحميل الصور باستخدام priority، مما يعني أن الصور المهمة ستُحمل أولاً. أيضاً، يمكنك استخدام placeholder='blur' لعرض صورة ضبابية مؤقتة أثناء تحميل الصورة الحقيقية، مما يحسن تجربة المستخدم بشكل كبير. لكن كن حذراً: استخدام placeholder='blur' يتطلب توفير صورة صغيرة جداً (حوالي ١٠ بايت) كملف base64، وهذا قد يزيد حجم الصفحة قليلاً إذا استخدمت الكثير منها.
// مثال على تحسين الصور في App Router
import Image from 'next/image';
export default function HeroSection() {
return (
<div>
<Image
src="/hero-image.jpg"
alt="صورة البطل"
width={1200}
height={600}
priority // تحميل الصورة أولاً
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAgDASIAAhEBAxEB/8QAGwAAAgMBAQEAAAAAAAAAAAAABAUCAwYBAAf/xAA0EAACAQMCBAQEBQQDAAAAAAABAgMABBEFIQYSMUETUWFxByKBkRQyocHwQrHR4RUj8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDxAA//2Q=="
/>
</div>
);
}هناك أيضاً تقنية تسمى Code Splitting، التي تسمح لك بتحميل أجزاء من الكود فقط عند الحاجة إليها. في App Router، هذا يحدث تلقائياً للصفحات، حيث يتم تحميل كل صفحة بشكل مستقل. لكن يمكنك أيضاً استخدام dynamic import لتحميل مكونات معينة فقط عند الحاجة إليها. مثلاً، إذا كان لديك مكون ثقيل مثل محرر النصوص، يمكنك تحميله فقط عندما يضغط المستخدم على زر معين. هذا يقلل من حجم الحزمة الأولية ويحسن أداء التطبيق بشكل كبير.
// مثال على Code Splitting باستخدام dynamic import
import dynamic from 'next/dynamic';
// تحميل المكون فقط عند الحاجة
const HeavyEditor = dynamic(() => import('@/components/HeavyEditor'), {
loading: () => <p>جارٍ تحميل المحرر...</p>,
ssr: false // تعطيل SSR لهذا المكون
});
export default function CreatePostPage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button {() => setShowEditor(true)}>افتح المحرر</button>
{showEditor && <HeavyEditor />}
</div>
);
}بعد بناء أكثر من عشرة مشاريع باستخدام App Router في العامين الماضيين، أستطيع القول بثقة: هذا النظام ليس مجرد تحديث، بل ثورة في طريقة بناء التطبيقات الحديثة. لكن لكي تستفيد منه حقاً، يجب أن تتوقف عن التفكير فيه كإصدار مطور من Pages Router وتبدأ في فهم النموذج الجديد من الأساس. نصيحتي الأولى: ابدأ مشروعاً جديداً من الصفر باستخدام App Router ولا تحاول ترحيل مشروع قديم إلا إذا كان صغيراً جداً. الترحيل الكامل قد يستغرق وقتاً أطول من بناء المشروع من جديد، خاصة إذا كنت تستخدم الكثير من المكتبات التي تعتمد على Client Components.
نصيحة أخرى مهمة: لا تخف من استخدام Server Components قدر الإمكان. الكثير من المطورين ما زالوا يكتبون كل شيء كClient Components لأنهم معتادون على هذا النمط، لكنهم بذلك يخسرون الفوائد الرئيسية لـ App Router. تذكر أن Server Components ليست مجرد طريقة لجلب البيانات، بل هي طريقة لإعادة التفكير في بنية التطبيق بالكامل. ابدأ بكتابة كل شيء كServer Component، ثم حول فقط المكونات التي تحتاج إلى التفاعل مع المستخدم إلى Client Components باستخدام 'use client'. هذا النهج سيجعلك تستفيد من الأداء والأمان الذي تقدمه Server Components بشكل كامل.
أخيراً، لا تتجاهل أهمية فهم كيفية عمل التخزين المؤقت في App Router. الكثير من المشاكل التي تواجهها الفرق ناتجة عن عدم فهم كيفية عمل Data Cache وFull Route Cache. ابدأ بتعطيل التخزين المؤقت لكل شيء باستخدام cache: 'no-store'، ثم قم بتفعيله تدريجياً للبيانات التي لا تتغير كثيراً. واستخدم أدوات مثل next dev --turbo لمراقبة أداء التطبيق وتحديد المشاكل المحتملة قبل أن تصل إلى الإنتاج.
App Router ليس مجرد أداة جديدة، بل هو طريقة جديدة للتفكير في بناء التطبيقات. إذا استخدمته بالطريقة الصحيحة، ستجد نفسك تبني تطبيقات أسرع وأكثر أماناً وأقل تعقيداً من أي وقت مضى.
— مهندس برمجيات في شركة عالمية