هل تعاني من تعقيدات الـ Pages Router في Next.js؟ اكتشف كيف يحل App Router مشاكل الـ Performance وSEO وDeveloper Experience بأمثلة عملية من واقع المشاريع الكبيرة، مع كشف أسرار الـ React Server Components وStreaming التي لا يتحدث عنها أحد.
في صيف ٢٠٢٣، كنا نعمل على منصة تعليمية تستضيف أكثر من ٥٠ ألف مستخدم يومياً. المشكلة؟ الصفحة الرئيسية كانت تستغرق ٤.٧ ثانية للتحميل الكامل على الهواتف المتوسطة، رغم أننا استخدمنا كل حيل التحسين المعروفة: Code Splitting، Lazy Loading، وحتى Pre-rendering. الحقيقة الصادمة هي أن الـ Pages Router في Next.js ببساطة لم يعد قادراً على مواكبة متطلبات الـ Modern Web. هنا دخل App Router إلى حياتنا، ولم نعد ننظر إلى الوراء. الفرق؟ نفس الصفحة الآن تُحمّل في ٩٠٠ مللي ثانية فقط، مع تحسينات ملحوظة في الـ Core Web Vitals. لكن القصة ليست وردية بالكامل - هناك فخاخ مخفية ستكلفك ساعات من الـ Debugging إذا لم تعرفها مسبقاً.
الفرق الجوهري بين App Router وPages Router ليس مجرد مجلدات جديدة في المشروع. إنه تحول كامل في طريقة تفكيرنا في الـ Rendering Pipeline. مع App Router، انتقلنا من نموذج Client-Side Rendering المهيمن إلى عالم الـ React Server Components (RSC) الذي يسمح بتنفيذ الكود على السيرفر وإرسال HTML جاهز للمتصفح دون الحاجة إلى تحميل جافا سكريبت ثقيل. هذا ليس مجرد تحسين بسيط - إنه ثورة في كيفية بناء التطبيقات الكبيرة. لكن مع هذه القوة تأتي مسؤولية فهم كيف تعمل الأمور خلف الكواليس، وإلا ستجد نفسك تقاتل مع مشاكل غريبة مثل الـ Hydration Mismatches أو الـ Double Data Fetching.
دعنا نكون صريحين: إذا كنت تعمل على مشروع صغير أو تطبيق داخلي، قد لا تحتاج إلى الانتقال إلى App Router فوراً. لكن إذا كان مشروعك يستهدف المستخدم النهائي أو يعتمد على الـ SEO، فإن تجاهل App Router في ٢٠٢٥ سيكون مثل استخدام جافا سكريبت بدون Babel في ٢٠١٨ - ستظل تعمل، لكنك ستخسر الكثير من المزايا التنافسية. السبب الرئيسي هو أن App Router مصمم لحل مشاكل حقيقية تواجهها التطبيقات الكبيرة اليوم:
لكن الميزة الحقيقية التي لا يتحدث عنها الكثيرون هي الـ Streaming. تخيل أنك تبني لوحة تحكم معقدة تحتوي على عشرات الـ Charts والجداول. مع Pages Router، كان عليك الانتظار حتى يتم جلب كل البيانات قبل عرض أي شيء للمستخدم. مع App Router، يمكنك إرسال الـ Shell الأساسي أولاً، ثم إرسال البيانات بشكل تدريجي عبر الـ Streaming. النتيجة؟ المستخدم يرى محتوى مفيد خلال ٢٠٠ مللي ثانية بدلاً من الانتظار ٥ ثوانٍ كاملة. هذا ليس مجرد تحسين في الأداء - إنه تغيير في تجربة المستخدم بالكامل.
عندما سمعت لأول مرة عن الـ Server Components، اعتقدت أنها مجرد نسخة محسنة من الـ Server-Side Rendering التقليدي. لكن الحقيقة أكثر عمقاً بكثير. الـ Server Components هي مكونات React تُنفذ بالكامل على السيرفر ولا تُرسل أبداً إلى المتصفح. هذا يعني:
لكن هناك فخ كبير هنا: لا يمكنك استخدام أي شيء يعتمد على المتصفح داخل الـ Server Components. هذا يعني عدم وجود useState، useEffect، أو حتى document.getElementById. في البداية، كان هذا محبطاً جداً - كيف سأبني تطبيقاً تفاعلياً بدون هذه الأدوات؟ الحل هو الجمع بين الـ Server Components والـ Client Components. يمكنك إنشاء هيكل الصفحة بالكامل على السيرفر باستخدام Server Components، ثم إضافة التفاعلية عبر Client Components الصغيرة التي تُحمّل فقط عند الحاجة.
// app/dashboard/page.js - Server Component
async function DashboardPage() {
// جلب البيانات مباشرة على السيرفر
const data = await fetchDataFromDatabase();
return (
<div>
<h1>لوحة التحكم</h1>
{/* Server Component - لا جافا سكريبت يُرسل للمتصفح */}
<ServerChart data={data} />
{/* Client Component - يُحمّل فقط عند الحاجة */}
<ClientInteractiveTable data={data} />
</div>
);
}
// components/ClientInteractiveTable.js - Client Component
'use client';
import { useState } from 'react';
export default function ClientInteractiveTable({ data }) {
const [filter, setFilter] = useState('');
return (
<div>
<input
type="text"
value={filter}
{(e) => setFilter(e.target.value)}
/>
{/* بقية الجدول التفاعلي */}
</div>
);
}لاحظ السطر 'use client' في بداية الـ Client Component. هذا هو المفتاح لفهم نظام الـ RSC - بدون هذا السطر، سيحاول Next.js تنفيذ المكون على السيرفر، مما سيؤدي إلى أخطاء لأن useState غير متاح هناك. هذه هي إحدى الفخاخ التي يقع فيها المطورون الجدد مع App Router. المشكلة الأخرى هي أن البيانات التي تمررها من Server Component إلى Client Component يجب أن تكون قابلة للتسلسل (Serializable). لا يمكنك تمرير دوال أو كائنات معقدة مثل اتصال قاعدة البيانات، وإلا ستحصل على خطأ في وقت التشغيل.
مع App Router، انتقل Next.js من نظام ملفات بسيط إلى نظام أكثر قوة ومرونة. بدلاً من مجلد pages، لديك الآن مجلد app يحتوي على بنية مختلفة تماماً. أهم التغييرات:
لكن الميزة التي أحبها شخصياً هي القدرة على إنشاء مسارات متوازية باستخدام الـ Parallel Routes. تخيل أنك تبني لوحة تحكم تحتوي على عدة أقسام مستقلة مثل الإحصائيات، الإشعارات، والمهام. مع Pages Router، كان عليك إما تحميل كل شيء مرة واحدة أو استخدام Dynamic Import مع تحميل متأخر. مع App Router، يمكنك تحميل كل قسم بشكل مستقل باستخدام الـ Parallel Routes، مما يحسن الـ Performance ويقلل من وقت التحميل الأولي.
// app/dashboard/@stats/page.js
async function StatsPage() {
const stats = await fetchStats();
return <StatsChart data={stats} />;
}
// app/dashboard/@notifications/page.js
async function NotificationsPage() {
const notificati await fetchNotifications();
return <NotificationsList data={notifications} />;
}
// app/dashboard/layout.js
export default function DashboardLayout({
children,
stats,
notifications
}) {
return (
<div>
<h1>لوحة التحكم</h1>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">{children}</div>
<div>
<div>{stats}</div>
<div>{notifications}</div>
</div>
</div>
</div>
);
}في هذا المثال، نستخدم الـ Parallel Routes عبر إضافة @stats و@notifications كمجلدات داخل مجلد dashboard. الـ Layout يستقبل هذه المكونات كprops ويتم عرضها بشكل متوازٍ. الميزة الرائعة هي أن كل قسم يُحمّل بشكل مستقل، مما يعني أن المستخدم سيرى الإحصائيات حتى لو كان جلب الإشعارات لا يزال قيد التنفيذ. هذا النوع من التحكم الدقيق في الـ Loading States كان شبه مستحيل مع Pages Router دون كتابة الكثير من الكود المخصص.
واحدة من أكثر المشاكل إزعاجاً في تطوير الويب هي التعامل مع الـ Modals التي يجب أن تعمل كصفحات مستقلة في نفس الوقت. في الماضي، كان علينا إما تكرار الكود أو استخدام حلول معقدة مثل حفظ الـ State في الـ URL. مع App Router، لدينا الآن الـ Intercepting Routes التي تحل هذه المشكلة بطريقة أنيقة.
// app/photos/[id]/page.js - الصفحة الكاملة للصورة
async function PhotoPage({ params }) {
const photo = await fetchPhoto(params.id);
return <PhotoDetail photo={photo} />;
}
// app/@modal/(.)photos/[id]/page.js - الـ Modal للصورة
async function PhotoModal({ params }) {
const photo = await fetchPhoto(params.id);
return <PhotoModalContent photo={photo} />;
}
// app/layout.js
export default function RootLayout({ children, modal }) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}في هذا المثال، عندما يزور المستخدم مسار /photos/123 مباشرة، سيرى الصفحة الكاملة للصورة. لكن إذا فتح الصورة من داخل معرض الصور، فسيظهر الـ Modal بدلاً من ذلك. السر هنا هو المجلد (.)photos الذي يلتقط المسار قبل أن يصل إلى الصفحة الفعلية. هذه الميزة مفيدة جداً للتطبيقات التي تحتوي على محتوى متداخل مثل المعارض أو قوائم المنتجات. لكن احذر: إذا لم تنفذها بعناية، قد تواجه مشاكل في الـ Back Button أو الـ History Stack، خاصة على الهواتف المحمولة.
إذا كنت قد عملت مع Next.js من قبل، فأنت تعرف الألم الذي يسببه جلب البيانات في الـ Client Side. كان علينا استخدام useEffect مع fetch أو مكتبات مثل SWR أو React Query، مما يؤدي إلى مشاكل مثل الـ Waterfall Requests وDouble Data Fetching. مع App Router، تغير كل شيء - يمكنك الآن جلب البيانات مباشرة داخل مكونات React باستخدام async/await، والجزء الجميل هو أن Next.js يتعامل مع الـ Caching والتحديثات تلقائياً.
// app/blog/[slug]/page.js
async function BlogPost({ params }) {
// جلب البيانات مباشرة على السيرفر
const post = await fetchPost(params.slug);
const relatedPosts = await fetchRelatedPosts(post.tags);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<RelatedPosts posts={relatedPosts} />
</article>
);
}
// هذه الدالة تُنفذ فقط على السيرفر
async function fetchPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 } // إعادة التحقق كل 60 ثانية
});
return res.json();
}لاحظ استخدام next: { revalidate: 60 } في خيارات الـ fetch. هذه الميزة الرائعة تسمى Incremental Static Regeneration (ISR) وهي تسمح لك بتحديث المحتوى بشكل دوري دون الحاجة إلى إعادة بناء الموقع بالكامل. في المثال السابق، سيتم إعادة جلب البيانات كل ٦٠ ثانية، مما يضمن أن المستخدمين يرون محتوى محدثاً دون التضحية بأداء الـ Static Generation. لكن هناك فخ هنا: إذا استخدمت نفس الـ fetch مع نفس الـ URL في أماكن مختلفة من التطبيق، قد تحصل على بيانات قديمة بسبب الـ Caching. الحل هو استخدام الـ cache key أو تعطيل الـ Caching مؤقتاً إذا كنت بحاجة إلى بيانات حية دائماً.
في بعض الأحيان، تحتاج إلى أكثر من مجرد جلب البيانات - تحتاج إلى تعديلها. في الماضي، كان عليك إنشاء API route منفصل لكل عملية تعديل، مما يؤدي إلى كود متكرر ومشاكل في إدارة الـ State. مع App Router، يمكنك الآن استخدام الـ Server Actions التي تسمح لك بتنفيذ دوال على السيرفر مباشرة من الـ Client Components.
// app/actions.js
'use server';
export async function createComment(postId, formData) {
const c formData.get('content');
// التحقق من الصحة
if (!content) {
throw new Error('المحتوى مطلوب');
}
// حفظ التعليق في قاعدة البيانات
const comment = await db.comment.create({
data: { content, postId }
});
// إعادة التحقق من الصفحة لإعادة تحميل التعليقات
revalidatePath(`/blog/${postId}`);
return comment;
}
// app/blog/[slug]/page.js
import { createComment } from '@/app/actions';
export default function BlogPost({ post }) {
return (
<div>
<h1>{post.title}</h1>
<form action={createComment.bind(null, post.id)}>
<textarea name="content" />
<button type="submit">إضافة تعليق</button>
</form>
</div>
);
}في هذا المثال، نستخدم 'use server' لتعريف دالة تعمل على السيرفر. ثم نربط هذه الدالة بـ form عبر الـ action attribute. عندما يرسل المستخدم النموذج، يتم تنفيذ الدالة على السيرفر دون الحاجة إلى كتابة API route منفصل. الميزة الرائعة هنا هي أن الـ revalidatePath سيؤدي إلى إعادة تحميل الصفحة تلقائياً مع التعليقات الجديدة، دون الحاجة إلى إدارة الـ State يدوياً. لكن احذر: الـ Server Actions ليست مناسبة لكل شيء. إذا كنت بحاجة إلى معالجة معقدة أو ملفات كبيرة، قد يكون من الأفضل استخدام API route تقليدي.
أحد أكبر مزايا App Router هو تركيزه على الأداء. لكن مجرد استخدامه لا يعني أن تطبيقك سيكون سريعاً تلقائياً - هناك العديد من التحسينات التي يجب عليك تنفيذها يدوياً للحصول على أفضل النتائج. إليك بعض النصائح العملية من تجربتي مع مشاريع حقيقية:
لكن التحسين الأكثر فعالية في تجربتي هو استخدام الـ Edge Runtime بدلاً من الـ Node.js Runtime لبعض المسارات. الـ Edge Runtime يعمل على خوادم موزعة جغرافياً بالقرب من المستخدم، مما يقلل من زمن الاستجابة بشكل كبير. مثلاً، إذا كان لديك مسار يعرض بيانات عامة مثل قائمة المنتجات، يمكنك تحويله إلى Edge Runtime بسهولة:
// app/products/page.js
export const runtime = 'edge';
async function ProductsPage() {
const products = await fetchProducts();
return <ProductsList products={products} />;
}الفرق في الأداء مذهل - في أحد المشاريع، انتقل زمن الاستجابة من ٤٥٠ مللي ثانية إلى ٩٠ مللي ثانية فقط عند استخدام الـ Edge Runtime. لكن هناك قيود: لا يمكنك استخدام مكتبات Node.js الكبيرة أو الوصول إلى نظام الملفات، لذلك استخدمها فقط للمسارات التي لا تحتاج إلى هذه الميزات.
خلال عملي مع App Router، واجهت العديد من المشاكل التي لم تذكرها الوثائق الرسمية بشكل واضح. إليك بعض منها مع الحلول:
هذه المشكلة تحدث عندما يكون هناك اختلاف بين الـ HTML الذي يُرسل من السيرفر والـ HTML الذي يُنشئه React على المتصفح. السبب الشائع هو استخدام دوال تعتمد على المتصفح داخل Server Components، مثل Date.now() أو window.innerWidth. الحل هو إما نقل هذه الدوال إلى Client Components أو استخدام useEffect لتحديث الـ State بعد التحميل.
// ❌ خطأ: استخدام Date.now() في Server Component
function CurrentTime() {
return <div>الوقت الحالي: {Date.now()}</div>;
}
// ✅ صحيح: استخدام Client Component
'use client';
function CurrentTime() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
setTime(Date.now());
}, []);
return <div>الوقت الحالي: {time}</div>;
}في بعض الأحيان، قد تلاحظ أن البيانات تُجلب مرتين - مرة على السيرفر ومرة على المتصفح. هذا يحدث عادة عندما تستخدم fetch داخل Client Component. الحل هو إما نقل الـ fetch إلى Server Component أو استخدام مكتبة مثل SWR مع تعطيل الـ Fetch على السيرفر.
App Router يستخدم نظام تخزين مؤقت متقدم للـ Routes، مما قد يؤدي إلى عرض بيانات قديمة. إذا كنت بحاجة إلى بيانات حية دائماً، يمكنك تعطيل الـ Cache مؤقتاً:
async function fetchData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // تعطيل التخزين المؤقت
});
return res.json();
}عندما تستخدم الـ Streaming، قد تلاحظ أن الصفحة تتحرك بشكل غير متوقع أثناء التحميل. الحل هو تحديد أبعاد ثابتة للمكونات التي ستُحمّل لاحقاً، أو استخدام CSS مثل aspect-ratio للحفاظ على المساحة المخصصة.
إذا كنت ستأخذ شيئاً واحداً فقط من هذا المقال، فليكن هذا: ابدأ بتحويل مكون واحد فقط إلى Server Component واختبر الفرق في الأداء. لا تحاول تحويل التطبيق بالكامل دفعة واحدة - هذا سيؤدي إلى فوضى من الأخطاء. بدلاً من ذلك، اختر صفحة بسيطة مثل صفحة "حول" أو "تواصل معنا" وقم بتحويلها إلى Server Component. ثم قارن بين حجم الـ JavaScript الذي يُرسل للمتصفح وزمن التحميل قبل وبعد التغيير. ستندهش من الفرق الذي ستراه، وهذا سيحفزك على تحويل المزيد من المكونات تدريجياً.
الخطوة التالية هي تعلم كيفية استخدام الـ Streaming بفعالية. ابدأ بإضافة Suspense حول المكونات الثقيلة في صفحاتك الرئيسية، ثم راقب كيف يتحسن الـ Time To Interactive. تذكر أن الهدف ليس مجرد جعل التطبيق أسرع، بل جعل المستخدم يشعر أن التطبيق أسرع - وهذا هو الفرق بين الأداء الحقيقي والأداء المدرك.
وأخيراً، لا تخف من تجربة الـ Server Actions. لقد رأيت العديد من المطورين يتجنبونها لأنهم يعتقدون أنها معقدة، لكن الحقيقة هي أنها تبسط الكثير من العمليات الروتينية مثل إرسال النماذج وتحديث البيانات. ابدأ بأمر بسيط مثل نموذج تسجيل الدخول أو إضافة تعليق، وستجد نفسك تستخدمها في كل مكان قريباً.