هل تعتقد أنك تعرف App Router؟ هذا الدليل يكشف لك التفاصيل الخفية خلف الكواليس: من الـ Server Components التي تعمل في الذاكرة إلى الـ Streaming الذي يسرع التطبيق 3 أضعاف، مع أمثلة عملية وحلول حقيقية للمشاكل التي تواجهها في الإنتاج.
في صيف ٢٠٢٣، قررت شركة ناشئة في دبي ترحيل تطبيقها من Pages Router إلى App Router. بعد شهرين من العمل، اكتشف الفريق أن الـ Time to First Byte انخفض من ٤٥٠ مللي ثانية إلى ١٢٠ مللي ثانية، لكن الـ Memory Usage ارتفع بنسبة ٣٠٪ بسبب تسريب في الـ Server Components. القصة لم تكن في الأداء فقط، بل في الطريقة التي يتعامل بها Next.js مع الـ Requests خلف الكواليس. اليوم، App Router ليس مجرد ميزة جديدة، بل هو إعادة تعريف لكيفية بناء تطبيقات React على الويب. إذا كنت لا تزال تفكر في App Router كبديل لـ Pages Router، فأنت تفوتك ثورة حقيقية في هندسة الواجهة الأمامية.
في هذا الدليل، لن نتكلم عن الأساسيات التي تغطيها وثائق Next.js. بدلاً من ذلك، سنغوص في التفاصيل التي لا يشرحها أحد: كيف يعمل الـ React Server Components على مستوى الـ Event Loop، لماذا الـ Streaming يمكن أن يجعل تطبيقك يبدو وكأنه يعمل على جهاز محلي حتى لو كان السيرفر في سنغافورة، وكيف تتجنب الـ Memory Leaks التي تصيب حتى أفضل الفرق. كل مثال هنا قابل للتطبيق مباشرة في مشاريع حقيقية، وكل نصيحة تأتي من تجربة فعلية في بيئات الإنتاج.
عندما انتقلت من Pages Router إلى App Router لأول مرة، ظننت أنني سأجد مجرد تغيير في هيكل المجلدات. لكن الحقيقة هي أن App Router يعيد تصميم كامل لكيفية تعامل Next.js مع الـ Requests. في Pages Router، كل صفحة هي ملف React واحد يتم تحويله إلى HTML على السيرفر ثم يرسل إلى العميل. لكن في App Router، الصفحة تتكون من مكونات متعددة، بعضها يعمل على السيرفر وبعضها على العميل، وكل منها له دورة حياة مختلفة. هذا التغيير ليس تجميلياً، بل هو تغيير في النموذج الذهني لبناء التطبيقات.
لنأخذ مثالاً عملياً: في Pages Router، إذا كان لديك صفحة تعرض بيانات من API، فإن الـ Data Fetching يحدث بالكامل على السيرفر، ثم يرسل HTML جاهز إلى العميل. لكن في App Router، يمكنك أن تجعل بعض المكونات تسترجع البيانات على السيرفر (Server Components) وبعضها على العميل (Client Components)، وكل ذلك يحدث بالتوازي. هذا يعني أن الـ Rendering يمكن أن يبدأ قبل أن تنتهي جميع طلبات البيانات، مما يقلل من وقت التحميل المدرك بشكل كبير. لكن هذا أيضاً يعني أنك بحاجة لفهم كيفية عمل الـ React Suspense و الـ Streaming، وإلا ستجد نفسك أمام شاشة بيضاء تنتظر البيانات إلى الأبد.
// مثال على صفحة في App Router تستخدم Server و Client Components
// app/page.tsx
import { Suspense } from 'react';
import ServerComponent from './ServerComponent';
import ClientComponent from './ClientComponent';
async function fetchData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
});
return res.json();
}
export default async function Page() {
const data = await fetchData();
return (
<div>
<h1>App Router Example</h1>
{/* Server Component: يتم رندرته على السيرفر */}
<ServerComponent data={data} />
{/* Client Component: يتم تحميله على العميل */}
<Suspense fallback={<div>Loading client data...</div>}>
<ClientComponent />
</Suspense>
</div>
);
}الـ Server Components هي أحد أكبر التغييرات في App Router، لكنها أيضاً الأكثر غموضاً. الكثير من المطورين يعتقدون أنها مجرد مكونات React تعمل على السيرفر، لكن الحقيقة أكثر تعقيداً. عندما تطلب صفحة تحتوي على Server Components، فإن Next.js لا يرسل HTML جاهزاً فقط، بل يرسل ما يسمى بـ "React Flight"، وهو تمثيل ثنائي للمكونات التي يجب أن تُرندر على العميل. هذا يعني أن الـ Client لا يحتاج لإعادة تنفيذ منطق الـ Server Components، بل فقط يرندر النتيجة النهائية.
لفهم ما يحدث خلف الكواليس، تخيل أن لديك مكون Server Component يسترجع بيانات من قاعدة بيانات ويظهرها في جدول. عندما يطلب المستخدم الصفحة، يحدث الآتي: ١) Next.js ينفذ الكود على السيرفر، بما في ذلك استدعاءات الـ API أو الـ Database Queries. ٢) بدلاً من تحويل المكون إلى HTML، يقوم Next.js بتحويل المكون إلى تنسيق ثنائي يمكن للعميل فهمه. ٣) يرسل هذا التنسيق إلى العميل مع تعليمات حول كيفية تحويله إلى DOM. ٤) العميل يستقبل البيانات الثنائية ويرندرها مباشرة، دون الحاجة لإعادة تنفيذ الكود. هذه العملية تقلل من كمية JavaScript التي ترسل إلى العميل، مما يحسن الأداء بشكل كبير، لكنها أيضاً تعني أن أي خطأ في Server Component يمكن أن يكسر الصفحة بالكامل دون أن يظهر في الـ Console.
// مثال على Server Component مع خطأ خفي
// app/ServerComponent.tsx
import { db } from './db';
export default async function ServerComponent() {
// هذا الاستعلام قد يفشل إذا كانت قاعدة البيانات غير متصلة
const users = await db.query('SELECT * FROM users');
// إذا فشل الاستعلام، لن يظهر الخطأ في الـ Console على العميل
// بل ستظهر صفحة خطأ 500
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}الخطأ الأكثر شيوعاً في Server Components هو افتراض أن الأخطاء ستظهر في الـ Console على العميل، لكنها في الواقع تظهر على السيرفر فقط. هذا يعني أنك بحاجة لاستراتيجيات مختلفة لمعالجة الأخطاء. أولاً، استخدم try/catch حول أي كود غير موثوق به، مثل استدعاءات الـ API أو الـ Database Queries. ثانياً، استخدم مكون Error Boundary مخصص للـ Server Components، لأن الـ Error Boundaries العادية لا تعمل معها. ثالثاً، استخدم أدوات مراقبة مثل Sentry أو Datadog لتتبع الأخطاء على السيرفر، لأنك لن تراها على العميل.
// Server Component مع معالجة الأخطاء
// app/ServerComponent.tsx
import { db } from './db';
export default async function ServerComponent() {
let users = [];
try {
users = await db.query('SELECT * FROM users');
} catch (error) {
console.error('Database query failed:', error);
// يمكنك إرسال الخطأ إلى خدمة مراقبة هنا
// sentry.captureException(error);
return <div>Failed to load users. Please try again later.</div>;
}
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}واحدة من أقوى الميزات في App Router هي القدرة على استخدام الـ Streaming مع الـ Suspense. الفكرة بسيطة: بدلاً من انتظار تحميل جميع البيانات قبل عرض الصفحة، يمكنك عرض أجزاء من الصفحة فور توفرها. هذا يعني أن المستخدم يرى محتوى مفيداً في وقت أبكر بكثير، حتى لو كانت بعض الأجزاء لا تزال تُحمّل. لكن خلف الكواليس، الـ Streaming يعمل على مستوى الـ HTTP Protocol، حيث يرسل السيرفر أجزاء من الـ HTML بشكل متتابع باستخدام الـ Chunked Transfer Encoding.
لتوضيح الفكرة، تخيل أنك تبني لوحة تحكم تعرض بيانات من ثلاثة مصادر مختلفة: المستخدمين، المنتجات، والإحصائيات. في Pages Router، كان عليك الانتظار حتى تنتهي جميع طلبات البيانات قبل عرض أي شيء. لكن في App Router، يمكنك عرض كل جزء فور تحميله باستخدام الـ Suspense. هذا لا يقلل فقط من الـ Time to First Byte، بل يجعل التطبيق يبدو وكأنه يعمل على جهاز محلي حتى لو كان السيرفر بطيئاً. لكن هناك فخ هنا: إذا لم تستخدم الـ Suspense بشكل صحيح، فقد ينتهي بك الأمر مع شاشة بيضاء تنتظر البيانات إلى الأبد، أو أسوأ، مع مكونات تظهر وتختفي بشكل عشوائي بسبب إعادة الـ Rendering.
// مثال على استخدام Streaming مع Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
import Users from './Users';
import Products from './Products';
import Stats from './Stats';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* كل مكون يستخدم Suspense لعرض محتوى فور تحميله */}
<Suspense fallback={<div>Loading users...</div>}>
<Users />
</Suspense>
<Suspense fallback={<div>Loading products...</div>}>
<Products />
</Suspense>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
</div>
);
}
// app/Users.tsx
async function fetchUsers() {
const res = await fetch('https://api.example.com/users', {
next: { revalidate: 30 } // Revalidate every 30 seconds
});
return res.json();
}
export default async function Users() {
const users = await fetchUsers();
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}أولاً، لا تضع جميع مكونات الصفحة داخل Suspense واحد. هذا يهزم الغرض من الـ Streaming، لأن الصفحة ستنتظر حتى ينتهي تحميل جميع المكونات. بدلاً من ذلك، ضع كل مكون بطيء داخل Suspense خاص به. ثانياً، تجنب استخدام Suspense مع مكونات تعتمد على بعضها البعض، لأن هذا قد يؤدي إلى حالة تسمى "Waterfall" حيث ينتظر كل مكون الآخر. ثالثاً، استخدم الـ Loading.js في مجلد الصفحة لعرض حالة تحميل عامة، لكن لا تعتمد عليه فقط، لأن الـ Suspense يعطي تحكم أدق.
من تجربتي، أكبر خطأ يرتكبه المطورون هو استخدام Suspense مع مكونات Client Components التي تستخدم useEffect أو useState. هذا لا يعمل كما تتوقع، لأن الـ Suspense مصمم للعمل مع الـ Server Components أو مكونات تستخدم React.lazy للتحميل الكسول. إذا كنت بحاجة لاستخدام Suspense مع Client Components، فاستخدم React.lazy بدلاً من ذلك.
في App Router، لديك ثلاث طرق رئيسية لاسترجاع البيانات: الـ Server Components، الـ Server Actions، والـ Route Handlers. كل منها له استخداماته الخاصة، واختيار الطريقة الخاطئة يمكن أن يؤدي إلى مشاكل في الأداء أو الأمان. الـ Server Components هي الخيار الافتراضي لاسترجاع البيانات التي تحتاجها لعرض الصفحة، لأنها تعمل على السيرفر ولا ترسل أي JavaScript إلى العميل. لكن ماذا لو كنت بحاجة لتحديث البيانات بعد تفاعل المستخدم، مثل إرسال نموذج؟ هنا يأتي دور الـ Server Actions.
الـ Server Actions هي دوال JavaScript تعمل على السيرفر ويمكن استدعاؤها مباشرة من مكونات العميل. هذا يعني أنك يمكنك إرسال نموذج أو تحديث بيانات دون الحاجة لكتابة API منفصل. لكن هناك مشكلة: الـ Server Actions تعمل فقط مع الـ POST Requests، ولا يمكنها التعامل مع الـ GET Requests أو الـ Streaming. أيضاً، إذا كنت بحاجة لإرسال بيانات كبيرة، فقد تواجه مشاكل في الأداء بسبب حجم الـ Payload. من ناحية أخرى، الـ Route Handlers هي دوال تعمل على السيرفر ويمكن استخدامها لبناء APIs كاملة، مثل التعامل مع الـ Webhooks أو إرسال بيانات إلى خدمات خارجية. لكنها تتطلب كتابة كود أكثر، ولا تدعم الـ Streaming مثل الـ Server Components.
// مثال على Server Action لتحديث البيانات
// app/actions.ts
'use server';
export async function updateUserName(userId: string, newName: string) {
// تحقق من صحة البيانات
if (!newName || newName.length < 3) {
return { error: 'Name must be at least 3 characters' };
}
// تحديث البيانات في قاعدة البيانات
try {
await db.query('UPDATE users SET name = $1 WHERE id = $2', [newName, userId]);
return { success: true };
} catch (error) {
console.error('Failed to update user:', error);
return { error: 'Failed to update user' };
}
}
// استخدام Server Action في مكون العميل
// app/UserForm.tsx
'use client';
import { updateUserName } from './actions';
export default function UserForm({ userId }: { userId: string }) {
const [name, setName] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
const result = await updateUserName(userId, name);
if (result.error) {
setStatus('error');
} else {
setStatus('success');
}
};
return (
<form {handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New name"
/>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Updating...' : 'Update'}
</button>
{status === 'error' && <p>Failed to update name</p>}
{status === 'success' && <p>Name updated successfully!</p>}
</form>
);
}عندما بدأت الفرق في اعتماد App Router، ظهرت مشكلة لم تكن متوقعة: الـ Memory Leaks. السبب؟ الـ Server Components تعمل على السيرفر، وإذا لم تكن حذراً، فقد تحتفظ بمراجع للبيانات أو الـ Closures التي لا يتم تحريرها أبداً. على سبيل المثال، إذا كان لديك Server Component يستدعي دالة خارجية تحتفظ بمرجع لقاعدة البيانات، فقد ينتهي بك الأمر مع اتصال مفتوح لقاعدة البيانات حتى بعد انتهاء الـ Request. هذا قد يؤدي إلى استهلاك زائد للذاكرة على السيرفر، خاصة في التطبيقات ذات الـ Traffic العالي.
مشكلة أخرى شائعة هي الـ Blocking Calls في Server Components. لأن Server Components تعمل بشكل متزامن (Synchronous) على السيرفر، فإن أي استدعاء بطيء لقاعدة البيانات أو API يمكن أن يجعل السيرفر بأكمله بطيئاً. تخيل أن لديك Server Component يستغرق ٥ ثوانٍ لاسترجاع البيانات من API بطيء. إذا كان لديك ١٠٠ مستخدم يطلبون الصفحة في نفس الوقت، فقد ينتهي بك الأمر مع ١٠٠ اتصال مفتوح ينتظر البيانات، مما يؤدي إلى توقف السيرفر. الحل؟ استخدم الـ Caching مع revalidate، أو استخدم الـ Streaming مع Suspense لعزل المكونات البطيئة.
// مثال على Server Component مع مشكلة Memory Leak
// app/LeakyComponent.tsx
import { db } from './db';
// هذه الدالة تحتفظ بمرجع لقاعدة البيانات
// مما قد يؤدي إلى تسريب الذاكرة
async function getData() {
// هذا الاستعلام قد يترك اتصال قاعدة البيانات مفتوحاً
return db.query('SELECT * FROM large_table');
}
export default async function LeakyComponent() {
const data = await getData();
return (
<div>
{data.map(item => (
<div key={item.id}>{item.value}</div>
))}
</div>
);
}
// الحل: استخدم مكتبة تدير الاتصالات بشكل صحيح
// app/FixedComponent.tsx
import { db } from './db';
export default async function FixedComponent() {
// استخدم مكتبة مثل 'pg' مع اتصال مُدار
const client = await db.connect();
try {
const { rows } = await client.query('SELECT * FROM large_table');
return (
<div>
{rows.map(item => (
<div key={item.id}>{item.value}</div>
))}
</div>
);
} finally {
// تأكد من إغلاق الاتصال
client.release();
}
}أولاً، استخدم أدوات مثل Next.js Analytics أو Vercel Speed Insights لمراقبة الـ Core Web Vitals. هذه الأدوات تعطيك نظرة شاملة على أداء تطبيقك في العالم الحقيقي، بما في ذلك الـ Time to First Byte، و الـ First Contentful Paint، و الـ Cumulative Layout Shift. ثانياً، استخدم أدوات مراقبة السيرفر مثل Prometheus أو Datadog لتتبع استخدام الذاكرة و الـ CPU على السيرفر. هذا يساعدك في اكتشاف الـ Memory Leaks أو الـ Blocking Calls قبل أن تؤثر على المستخدمين. ثالثاً، استخدم أدوات مثل Sentry لتتبع الأخطاء على السيرفر، خاصة في Server Components، لأن هذه الأخطاء لا تظهر على العميل.
من تجربتي، أكبر مشكلة في مراقبة الأداء هي تجاهل الـ Server-Side Metrics. الكثير من المطورين يركزون فقط على ما يحدث على العميل، لكنهم ينسون أن الـ Server Components تعمل على السيرفر، وأن أي بطء هناك يمكن أن يؤثر على تجربة المستخدم. لذلك، تأكد من مراقبة كل من الـ Client-Side و الـ Server-Side Metrics.
بعد أكثر من عام من العمل مع App Router في مشاريع حقيقية، هذه هي النصائح التي أتمنى أن أعرفها قبل أن أبدأ: أولاً، لا تحاول تحويل جميع مكوناتك إلى Server Components. استخدم Client Components للمكونات التفاعلية، و Server Components للبيانات الثابتة أو التي تحتاج للتحديث الدوري. ثانياً، استخدم الـ Streaming مع Suspense بحكمة، ولا تضع كل شيء داخل Suspense واحد. ثالثاً، راقب استخدام الذاكرة على السيرفر، خاصة إذا كنت تستخدم Server Components مع قواعد بيانات أو APIs خارجية. رابعاً، استخدم Server Actions للتفاعلات البسيطة، لكن انتقل إلى Route Handlers إذا كنت بحاجة لمزيد من التحكم. وأخيراً، لا تعتمد فقط على وثائق Next.js، لأن الكثير من التفاصيل الخفية لا تظهر إلا في بيئات الإنتاج.
App Router ليس مجرد ترقية لـ Pages Router، بل هو إعادة تفكير في كيفية بناء تطبيقات الويب. إذا استخدمت ميزاته بحكمة، يمكنك بناء تطبيقات أسرع وأكثر كفاءة. لكن إذا تجاهلت التفاصيل الخفية، فقد ينتهي بك الأمر مع تطبيق بطيء أو غير مستقر. الخطوة التالية؟ ابدأ بمشروع صغير، وجرب كل ميزة على حدة، وراقب الأداء بعناية. ، ستفهم حقاً قوة App Router ومخاطره.