هل ما زلت تستخدم Pages Router؟ في 2025، App Router ليس مجرد تحديث بل ثورة في بنية التطبيقات. سنفكك معاً كيف يعمل خلف الكواليس، ونبني مثالاً عملياً مع Server Components وStreaming، ونكشف الفخاخ التي يقع فيها حتى المطورون المحترفون.
في صيف 2023، كنت أعمل على منصة تعليمية لمعهد سعودي كبير. العميل أراد تحميل صفحات الدروس بسرعة البرق، حتى على شبكات 3G. استخدمت Pages Router مع getStaticProps، لكن المشكلة ظهرت عندما زاد عدد الدروس إلى 5000 درس: وقت البناء (Build Time) قفز إلى 45 دقيقة، وكل تعديل بسيط يتطلب إعادة بناء كامل. هنا أدركت أن App Router ليس مجرد ميزة جديدة، بل هو إعادة تفكير جذرية في كيفية بناء التطبيقات. اليوم، نفس المشروع يبنى في أقل من دقيقتين، والصفحات تُحمّل قبل أن ينهي المستخدم حركة الماوس.
الفرق الأساسي بين App Router وPages Router ليس في المجلدات فقط، بل في نموذج التنفيذ. App Router يعتمد على React Server Components (RSC) بشكل افتراضي، وهذا يعني أن مكونات الصفحة تُنفذ على السيرفر وتُرسَل كبيانات ثنائية (Binary Payload) بدلاً من HTML كامل. هذا يقلل حجم الاستجابة من 120 كيلوبايت إلى 12 كيلوبايت في المتوسط، ويقلل وقت التفاعل (TTI) بنسبة 60% حسب تجاربنا في Nouvil. لكن هذا ليس كل شيء: App Router يدعم Streaming تلقائياً، مما يعني أن الصفحة تُعرض تدريجياً بينما البيانات لا تزال تُحمّل من قاعدة البيانات أو API خارجي.
عندما يكتب المستخدم عنوان URL مثل /dashboard/analytics، يبدأ Next.js رحلة معقدة خلف الكواليس. أولاً، يُحلل الراوتر المسار باستخدام نظام الملفات، لكنه هذه المرة لا يبحث عن ملف page.js فقط، بل عن مجلد كامل يحتوي على layout.js وpage.js وربما loading.js وerror.js. هذه الملفات تُعالج بشكل مختلف تماماً عن Pages Router. مثلاً، ملف layout.js يُنفذ على السيرفر ويُعاد استخدامه عبر صفحات متعددة، بينما page.js يمكن أن يكون Server Component أو Client Component حسب الحاجة.
المفاجأة الكبرى هي أن Next.js لا يرسل HTML كاملاً للمتصفح في البداية. بدلاً من ذلك، يرسل ما يسمى بـ React Flight Payload، وهو تنسيق ثنائي يحتوي على تعليمات لبناء شجرة المكونات على المتصفح. هذا التنسيق أصغر بكثير من HTML، ويحتوي فقط على البيانات الضرورية لعرض الصفحة. مثلاً، إذا كان لديك مكون يعرض قائمة المستخدمين، فإن الـ Payload يحتوي على البيانات فقط (مثل [{id: 1, name: 'أحمد'}, ...]) وليس على HTML كامل لكل عنصر. المتصفح يستخدم هذه البيانات لإعادة بناء المكونات باستخدام React Client Runtime، وهذا ما يجعل التطبيق سريعاً جداً حتى على الأجهزة الضعيفة.
// مثال على Server Component في App Router
// app/dashboard/users/page.js
import { db } from '@/lib/db';
async function UsersPage() {
// هذه الاستدعاءات تُنفذ على السيرفر فقط
const users = await db.user.findMany({
where: { active: true },
orderBy: { lastLogin: 'desc' },
take: 20
});
// لاحظ عدم وجود useState أو useEffect هنا
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">المستخدمون النشطون</h1>
<ul className="space-y-2">
{users.map(user => (
<li key={user.id} className="p-2 bg-gray-100 rounded">
{user.name} - آخر دخول: {user.lastLogin.toLocaleDateString()}
</li>
))}
</ul>
</div>
);
}
export default UsersPage;في المثال السابق، المكون UsersPage يُنفذ بالكامل على السيرفر. لا يوجد JavaScript يُرسل إلى المتصفح لهذا المكون، فقط البيانات الناتجة عن render تُحوّل إلى React Flight Payload. هذا يختلف تماماً عن Pages Router حيث كان كل مكون يُرسل ككود JavaScript إلى المتصفح حتى لو كان ثابتاً. لكن ماذا لو احتجنا إلى تفاعلية؟ هنا يأتي دور Client Components، التي تُضاف باستخدام 'use client' في بداية الملف.
الخطأ الشائع الذي أراه في المشاريع الجديدة هو استخدام Client Components لكل شيء
فقط لأن المطور اعتاد على ذلك في Pages Router. الحقيقة هي أن Server Components هي الخيار الافتراضي في App Router لسبب وجيه: الأداء. عندما تستخدم Server Component، فإن الكود يُنفذ على السيرفر ولا يُرسل أي JavaScript إلى المتصفح. هذا يعني أن المكون لا يشغل مساحة في ذاكرة المتصفح، ولا يحتاج إلى وقت للتنفيذ على الجهاز الضعيف. مثلاً، في منصة Nouvil، انتقلنا من 320 كيلوبايت من JavaScript إلى 80 كيلوبايت فقط بعد تحويل المكونات الثابتة إلى Server Components، وهذا قلل وقت التحميل الكامل للصفحة من 2.4 ثانية إلى 0.9 ثانية على شبكة 4G.
لكن هناك حالات لا يمكنك تجنب استخدام Client Components فيها. مثلاً، إذا كنت بحاجة إلى استخدام useState أو useEffect أو التعامل مع أحداث المستخدم مثل onClick، فعليك استخدام Client Component. لكن حتى هنا، يمكنك تقليل حجم JavaScript عن طريق فصل الجزء التفاعلي فقط في مكون منفصل. مثلاً، بدلاً من جعل الصفحة كاملة Client Component، اجعل فقط الزر أو القائمة المنسدلة Client Component. هذا ما يسمى بـ Islands Architecture، وهو نمط شائع في App Router.
// مثال على فصل Server Component وClient Component
// app/dashboard/analytics/page.js (Server Component)
import { getAnalyticsData } from '@/lib/analytics';
import InteractiveChart from './InteractiveChart';
async function AnalyticsPage() {
const data = await getAnalyticsData();
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">تحليل البيانات</h1>
<div className="mb-6">
{/* هذا الجزء ثابت وسيُعرض فوراً */}
<p>آخر تحديث: {data.lastUpdated.toLocaleString()}</p>
</div>
{/* هذا الجزء تفاعلي وسيُحمّل لاحقاً */}
<InteractiveChart initialData={data.chartData} />
</div>
);
}
export default AnalyticsPage;
// app/dashboard/analytics/InteractiveChart.js (Client Component)
'use client';
import { useState } from 'react';
import { LineChart } from '@/components/charts';
export default function InteractiveChart({ initialData }) {
const [timeRange, setTimeRange] = useState('week');
const [data, setData] = useState(initialData);
const handleRangeChange = async (range) => {
setTimeRange(range);
const resp await fetch(`/api/analytics?range=${range}`);
const newData = await response.json();
setData(newData);
};
return (
<div>
<div className="flex gap-2 mb-4">
{['day', 'week', 'month'].map(range => (
<button
key={range}
onClick={() => handleRangeChange(range)}
className={`px-3 py-1 rounded ${timeRange === range ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
{range}
</button>
))}
</div>
<LineChart data={data} />
</div>
);
}في المثال السابق، المكون InteractiveChart هو Client Component لأنه يحتاج إلى استخدام useState وonClick. لكن لاحظ أننا مررنا البيانات الأولية من Server Component، وهذا يقلل الحاجة إلى جلب البيانات مرة أخرى من المتصفح. هذه التقنية تسمى Pre-rendering with Client-side Hydration، وهي أحد أقوى ميزات App Router. لكن هناك فخ هنا: إذا مررت كمية كبيرة من البيانات الأولية، فقد يزيد حجم الـ Payload بشكل كبير. في أحد المشاريع، مررنا قائمة كاملة من 1000 عنصر كمُعامل أولي، مما زاد حجم الصفحة من 20 كيلوبايت إلى 220 كيلوبايت. الحل كان جلب البيانات الأولية فقط ثم استخدام API لجلب التفاصيل عند الطلب.
واحدة من أكثر الميزات التي أدهشتني في App Router هي دعم Streaming التلقائي. في Pages Router، إذا كان لديك بيانات بطيئة (مثل استعلام قاعدة بيانات معقد)، فإن الصفحة بأكملها تبقى بيضاء حتى تكتمل جميع البيانات. في App Router، يمكنك استخدام Suspense لفصل أجزاء الصفحة وعرضها تدريجياً. هذا يعني أن المستخدم يرى الجزء الثابت من الصفحة فوراً، بينما الأجزاء التي تعتمد على البيانات البطيئة تُعرض لاحقاً.
المثال الكلاسيكي هو لوحة تحكم تحتوي على عدة بطاقات: بطاقة المعلومات الأساسية، بطاقة التحليلات، وبطاقة المستخدمين النشطين. بدلاً من انتظار جميع البيانات، يمكنك عرض بطاقة المعلومات الأساسية فوراً، ثم بطاقة التحليلات عندما تكتمل، وأخيراً بطاقة المستخدمين النشطين. هذا يجعل التطبيق يشعر بأنه أسرع بكثير، حتى لو كان وقت التحميل الكامل هو نفسه. في تجربتنا مع منصة تعليمية، قللنا وقت الإدراك (Perceived Load Time) من 3.2 ثانية إلى 1.1 ثانية باستخدام هذه التقنية.
// مثال على استخدام Streaming مع Suspense
// app/dashboard/page.js
import { Suspense } from 'react';
import BasicInfo from './BasicInfo';
import AnalyticsCard from './AnalyticsCard';
import ActiveUsers from './ActiveUsers';
import Loading from './loading';
async function DashboardPage() {
return (
<div className="p-4 space-y-6">
<h1 className="text-2xl font-bold">لوحة التحكم</h1>
{/* هذا الجزء يُعرض فوراً */}
<BasicInfo />
{/* هذا الجزء يُعرض عندما تكتمل البيانات */}
<Suspense fallback={<Loading />}>
<AnalyticsCard />
</Suspense>
{/* هذا الجزء يُعرض لاحقاً */}
<Suspense fallback={<Loading />}>
<ActiveUsers />
</Suspense>
</div>
);
}
export default DashboardPage;
// app/dashboard/ActiveUsers.js
import { db } from '@/lib/db';
async function ActiveUsers() {
// محاكاة استعلام بطيء
await new Promise(resolve => setTimeout(resolve, 2000));
const users = await db.user.findMany({
where: { lastLogin: { gte: new Date(Date.now() - 86400000) } },
orderBy: { lastLogin: 'desc' },
take: 5
});
return (
<div className="p-4 bg-white rounded shadow">
<h2 className="text-xl font-semibold mb-2">المستخدمون النشطون اليوم</h2>
<ul className="space-y-2">
{users.map(user => (
<li key={user.id} className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-500 rounded-full"></div>
<span>{user.name}</span>
</li>
))}
</ul>
</div>
);
}في المثال السابق، لاحظ أن مكون ActiveUsers يحتوي على تأخير اصطناعي لمدة ثانيتين لمحاكاة استعلام قاعدة بيانات بطيء. لكن بفضل Suspense، فإن بقية الصفحة تُعرض فوراً، والمستخدم يرى محتوى الصفحة بينما البيانات لا تزال تُحمّل. هذا يختلف تماماً عن Pages Router حيث كان عليك استخدام مكتبات خارجية مثل react-query أو swr لتحقيق شيء مشابه. لكن هناك فخ هنا: إذا لم تستخدم Suspense بشكل صحيح، فقد ينتهي بك الأمر بمشكلة تسمى Waterfall Requests، حيث كل طلب يعتمد على الطلب السابق، مما يزيد وقت التحميل الكلي.
App Router يقدم نظام تخزين مؤقت (Caching) متطور جداً، لكنه أيضاً معقد. بشكل افتراضي، كل استدعاء لـ fetch في Server Component يُخزّن مؤقتاً تلقائياً. هذا يعني أنه إذا طلبت نفس البيانات مرتين، فإن الاستدعاء الثاني سيُعاد استخدامه من التخزين المؤقت بدلاً من الذهاب إلى قاعدة البيانات. هذا رائع للأداء، لكنه قد يسبب مشاكل إذا كانت البيانات تتغير بشكل متكرر. مثلاً، في منصة تداول الأسهم، إذا استخدمت التخزين المؤقت الافتراضي، فقد يرى المستخدم أسعاراً قديمة لمدة 30 ثانية.
لحسن الحظ، يمكنك التحكم في التخزين المؤقت باستخدام خيارات fetch. مثلاً، يمكنك استخدام { cache: 'no-store' } لتعطيل التخزين المؤقت تماماً، أو { next: { revalidate: 10 } } لإعادة التحقق من البيانات كل 10 ثوانٍ. لكن هنا يأتي الفخ: إذا استخدمت revalidate مع بيانات تتغير بشكل متكرر، فقد ينتهي بك الأمر بعرض بيانات قديمة للمستخدم لفترة قصيرة. في أحد المشاريع، استخدمنا revalidate: 5 لعرض أسعار العملات الرقمية، لكن هذا تسبب في وميض الصفحة (Page Flash) كل 5 ثوانٍ، مما أزعج المستخدمين. الحل كان استخدام WebSocket لتحديث البيانات في الوقت الفعلي بدلاً من الاعتماد على revalidate.
// التحكم في التخزين المؤقت في App Router
// app/api/prices/route.js
import { NextResponse } from 'next/server';
export async function GET() {
// تعطيل التخزين المؤقت تماماً
const resp await fetch('https://api.crypto.com/prices', {
cache: 'no-store'
});
const data = await response.json();
return NextResponse.json(data);
}
// app/dashboard/prices/page.js
async function PricesPage() {
// إعادة التحقق كل 10 ثوانٍ
const response = await fetch('http://localhost:3000/api/prices', {
next: { revalidate: 10 }
});
const prices = await response.json();
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">أسعار العملات الرقمية</h1>
<ul className="space-y-2">
{prices.map(price => (
<li key={price.symbol} className="p-2 bg-gray-100 rounded">
{price.symbol}: {price.value} USD
</li>
))}
</ul>
</div>
);
}هناك أيضاً نوع آخر من التخزين المؤقت في App Router يسمى Router Cache، وهو يخزن نتائج الـ RSC Payloads في ذاكرة المتصفح لمدة 5 دقائق افتراضياً. هذا يعني أنه إذا عاد المستخدم إلى صفحة زارها مؤخراً، فإنها تُعرض فوراً بدون طلب جديد للسيرفر. هذا رائع للأداء، لكنه قد يسبب مشاكل إذا كانت البيانات تتغير بشكل متكرر. مثلاً، في تطبيق إدارة المهام، إذا عاد المستخدم إلى قائمة المهام بعد تعديلها، فقد يرى النسخة القديمة لمدة 5 دقائق. يمكنك التحكم في هذا التخزين باستخدام router.refresh() في Client Components، أو باستخدام revalidatePath أو revalidateTag في Server Components.
خلال العام الماضي، رأيت العديد من المطورين يقعون في نفس الفخاخ عند الانتقال إلى App Router. الخطأ الأول هو تجاهل الفرق بين Server Components وClient Components. مثلاً، محاولة استخدام useState في Server Component سيؤدي إلى خطأ في وقت البناء. الحل هو إما تحويل المكون إلى Client Component باستخدام 'use client'، أو إعادة هيكلة الكود لفصل المنطق التفاعلي في مكون منفصل.
الخطأ الثاني هو الاعتماد كثيراً على Client Components. مثلاً، تحويل صفحة كاملة إلى Client Component فقط لأنك تحتاج إلى زر واحد تفاعلي. هذا يضيع فائدة Server Components ويزيد حجم JavaScript بشكل غير ضروري. الحل هو استخدام Islands Architecture، حيث تفصل الأجزاء التفاعلية فقط في مكونات صغيرة منفصلة.
// مثال على التعامل مع الأخطاء في App Router
// app/dashboard/error.js
'use client';
import { useEffect } from 'react';
export default function Error({ error, reset }) {
useEffect(() => {
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="p-4 bg-red-50 text-red-800 rounded">
<h2 className="text-xl font-bold mb-2">حدث خطأ!</h2>
<p className="mb-4">{error.message}</p>
<button
{() => reset()}
className="px-4 py-2 bg-red-500 text-white rounded"
>
إعادة المحاولة
</button>
</div>
);
}Next.js يتطور بسرعة كبيرة، وApp Router هو محور هذا التطور. في مؤتمر Next.js Conf 2024، أعلن الفريق عن العديد من الميزات الجديدة التي ستجعل App Router أكثر قوة. أهمها هو دعم Partial Prerendering، الذي سيجمع بين مزايا Static Site Generation وServer-Side Rendering. هذا يعني أنه يمكنك جعل أجزاء من الصفحة ثابتة (مثل الهيدر والفوتر) بينما الأجزاء الديناميكية تُعرض من السيرفر. هذا سيحل مشكلة وقت البناء الطويل في المشاريع الكبيرة.
هناك أيضاً تركيز كبير على تحسين تجربة المطور. مثلاً، أداة جديدة تسمى next dev --turbo ستسرع وقت البناء والتحديث أثناء التطوير بنسبة تصل إلى 70%. كما أن هناك خطط لدعم WebAssembly بشكل أفضل في Server Components، مما سيسمح بتشغيل مكتبات مكتوبة بلغات مثل Rust أو Go مباشرة على السيرفر. هذا سيفتح الباب أمام أداء غير مسبوق للتطبيقات التي تحتاج إلى معالجة مكثفة، مثل تحرير الفيديو أو تحليل البيانات الضخمة.
لكن أكبر تغيير قادم هو دعم Server Actions بشكل رسمي. حالياً، Server Actions هي ميزة تجريبية، لكنها ستُدمج بالكامل في Next.js 15. هذا سيجعل التعامل مع النماذج والبيانات أسهل بكثير، حيث يمكنك استدعاء دالة على السيرفر مباشرة من Client Component بدون الحاجة إلى كتابة API routes يدوياً. مثلاً، بدلاً من كتابة route.js للتعامل مع إرسال نموذج، يمكنك ببساطة كتابة دالة async في ملف Server Component واستدعائها مباشرة من نموذج HTML.
// مثال على Server Actions في Next.js 15 (ميزة تجريبية حالياً)
// app/actions.js
'use server';
import { db } from '@/lib/db';
export async function createPost(title, content) {
// هذه الدالة تُنفذ على السيرفر
const post = await db.post.create({
data: { title, content }
});
return post;
}
// app/posts/create/page.js (Client Component)
'use client';
import { createPost } from '@/app/actions';
export default function CreatePost() {
async function handleSubmit(formData) {
const title = formData.get('title');
const c formData.get('content');
// استدعاء Server Action مباشرة من المتصفح
const post = await createPost(title, content);
alert(`تم إنشاء المنشور: ${post.id}`);
}
return (
<form action={handleSubmit} className="space-y-4">
<input
type="text"
name="title"
placeholder="العنوان"
className="w-full p-2 border rounded"
/>
<textarea
name="content"
placeholder="المحتوى"
className="w-full p-2 border rounded"
></textarea>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded"
>
إنشاء المنشور
</button>
</form>
);
}بعد عام كامل من العمل مع App Router في مشاريع حقيقية، هذه هي النصائح التي أتمنى لو عرفتها منذ البداية:
App Router ليس مجرد تحديث بسيط لـ Next.js، بل هو إعادة تفكير في كيفية بناء التطبيقات الحديثة. نعم، هناك منحنى تعلم، وستواجه مشاكل في البداية، لكن الفوائد تستحق الجهد. في Nouvil، انتقلنا بالكامل إلى App Router في جميع مشاريعنا الجديدة، ورأينا تحسناً ملحوظاً في الأداء وتجربة المطور. إذا كنت لا تزال تستخدم Pages Router، فابدأ بالتجربة الآن. ابدأ بمشروع صغير، جرب Server Components وStreaming، وسترى الفرق بنفسك.
الخطوة التالية؟ اختر مشروعاً صغيراً لديك، حوله إلى App Router، وقس الأداء قبل وبعد. ستندهش من النتائج.