هل تعتقد أنك تتقن Next.js App Router؟ جرب هذا: تطبيقك يعمل بشكل مثالي على localhost، لكن عند النشر على Vercel يتجمد عند أول طلب بيانات. السبب؟ سوء فهم عميق لكيفية عمل الـ Server Components وStreaming. هذا الدليل سيجعلك تفهم ما يحدث خلف الكواليس وتكتب كوداً لا يعلق أبداً.
في عام 2023، قررت شركة Airbnb ترحيل لوحة التحكم الداخلية الخاصة بها من Pages Router إلى App Router في Next.js. النتيجة؟ تحسن أداء الصفحة الرئيسية بنسبة 40%، لكن ظهرت مشكلة غريبة: عند تحميل الصفحة لأول مرة، كان المستخدم يرى شاشة بيضاء لمدة 3-5 ثوانٍ قبل ظهور أي محتوى. المشكلة لم تكن في الـ API البطيء أو حجم الجافاسكريبت، بل في سوء فهم فريق التطوير لكيفية عمل Streaming في Server Components. هذا المثال ليس حالة نادرة، بل هو القاعدة في معظم المشاريع التي تنتقل إلى App Router دون فهم عميق لآلية عمله.
الـ App Router ليس مجرد تحديث بسيط لـ Next.js، بل هو إعادة تفكير كاملة في كيفية بناء التطبيقات. المشكلة الأكبر التي يواجهها المطورون هي أنهم يحاولون استخدامه بنفس العقلية القديمة للـ Pages Router أو حتى React التقليدي. النتيجة؟ تطبيقات تبدو وكأنها تعمل، لكنها في الواقع تعاني من مشاكل أداء خفية، مثل تحميل مكونات غير ضرورية على العميل، أو إرسال بيانات ضخمة عبر الشبكة دون داعٍ، أو حتى تسرب الذاكرة بسبب سوء إدارة الـ Server Components. في هذا الدليل، سنفكك كل جزء من الـ App Router، بدءاً من كيفية عمل الـ Server Components خلف الكواليس، وصولاً إلى أفضل الممارسات لتجنب الكوارث في الإنتاج.
الكثير من المطورين يعتقدون أن الـ Server Components هي مجرد مكونات React تعمل على السيرفر بدلاً من المتصفح. هذا الفهم السطحي يؤدي إلى كوارث في الأداء. الحقيقة هي أن الـ Server Components هي أشبه بـ "تعليمات بناء" يتم تنفيذها على السيرفر، والناتج هو JSON يصف هيكل المكون وليس HTML جاهز. عندما تكتب مكوناً مثل هذا:
// app/page.js
async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return (
<div>
<h1>{json.title}</h1>
<p>{json.description}</p>
</div>
);
}
export default Page;ما يحدث خلف الكواليس هو أن Next.js يحول هذا المكون إلى JSON يشبه هذا:
{
"type": "div",
"props": {
"children": [
{
"type": "h1",
"props": {
"children": "عنوان من API"
}
},
{
"type": "p",
"props": {
"children": "وصف من API"
}
}
]
}
}هذا الـ JSON هو ما يتم إرساله إلى المتصفح، وليس HTML جاهز. المتصفح يستخدم هذا الـ JSON لإعادة بناء المكون باستخدام React على العميل. الفرق الرئيسي هنا هو أن المتصفح لا يحتاج إلى تنفيذ أي كود جافاسكريبت للحصول على البيانات، لأن البيانات موجودة بالفعل في الـ JSON. لكن هذا يأتي بتكلفة: إذا كان لديك مكون كبير يحتوي على الكثير من البيانات، فإن حجم الـ JSON يمكن أن يصبح ضخماً جداً، مما يؤدي إلى بطء في التحميل. هذا هو السبب في أن الكثير من المطورين يشكون من أن تطبيقاتهم أصبحت أبطأ بعد الانتقال إلى App Router، رغم أن النظرية تقول العكس.
الحل يكمن في فهم مفهوم الـ Streaming. بدلاً من انتظار كل البيانات لتكون جاهزة قبل إرسال أي شيء إلى المتصفح، يمكنك إرسال أجزاء من الصفحة تدريجياً. هذا هو ما يفعله Next.js تلقائياً عندما تستخدم مكونات متداخلة. على سبيل المثال، إذا كان لديك صفحة تحتوي على مكون رئيسي ومكون جانبي، يمكنك جعل كل منهما يعمل بشكل مستقل:
// app/page.js
export default async function Page() {
return (
<div>
<MainContent />
<Sidebar />
</div>
);
}
// app/MainContent.js
async function MainContent() {
const data = await fetch('https://api.example.com/main');
return <div>{data.content}</div>;
}
// app/Sidebar.js
async function Sidebar() {
const data = await fetch('https://api.example.com/sidebar');
return <div>{data.items}</div>;
}في هذا المثال، الـ MainContent والـ Sidebar هما مكونان مستقلان. Next.js سيرسل الـ JSON لكل مكون بمجرد أن يصبح جاهزاً، بدلاً من انتظار كل البيانات. هذا يعني أن المستخدم سيرى جزء من الصفحة قبل أن تكتمل بقية البيانات. لكن هناك مشكلة شائعة هنا: إذا كان لديك مكون رئيسي يعتمد على بيانات من مكون فرعي، فإنك ستضطر إلى انتظار كل البيانات، مما يفقدك فائدة الـ Streaming. الحل هو إعادة تصميم مكوناتك بحيث تكون مستقلة قدر الإمكان.
الـ Client Components هي المكونات التي يتم تنفيذها على المتصفح، وهي ضرورية للتفاعل مع المستخدم. لكن الكثير من المطورين يستخدمونها بشكل مفرط، مما يؤدي إلى إرسال كميات ضخمة من الجافاسكريبت إلى المتصفح. المشكلة الأكبر هي أن الكثير من المكونات التي يتم وضع علامة "use client" عليها لا تحتاج فعلياً إلى هذه العلامة. على سبيل المثال، مكون مثل هذا:
'use client';
export default function Button({ children }) {
return <button>{children}</button>;
}هذا المكون لا يحتاج إلى علامة "use client" لأنه لا يحتوي على أي تفاعل مع المستخدم. مجرد وضع هذه العلامة سيجعله يتم تحميله على العميل، مما يزيد من حجم الجافاسكريبت المرسل. القاعدة الذهبية هي: إذا كان المكون لا يستخدم أي من hooks مثل useState أو useEffect أو أي مكتبات تعتمد على DOM مثل react-hook-form، فلا تضع عليه علامة "use client". حتى إذا كان المكون يستخدم useState، يمكنك أحياناً تجنب وضع العلامة إذا كان التفاعل محدوداً ويمكن تنفيذه باستخدام Server Actions.
الحل هو استخدام Server Actions بدلاً من Client Components للتفاعلات البسيطة. على سبيل المثال، إذا كان لديك نموذج تسجيل دخول بسيط، يمكنك كتابته بالكامل على السيرفر:
// app/login/page.js
export default function LoginPage() {
async function handleSubmit(formData) {
'use server';
const email = formData.get('email');
const password = formData.get('password');
// منطق تسجيل الدخول هنا
}
return (
<form action={handleSubmit}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">تسجيل الدخول</button>
</form>
);
}في هذا المثال، الـ form يتم إرساله إلى السيرفر بدون الحاجة إلى أي جافاسكريبت على العميل. هذا يقلل من حجم الجافاسكريبت المرسل ويحسن أداء الصفحة. لكن هناك مشكلة شائعة هنا: إذا كنت بحاجة إلى تحديث جزء من الصفحة بعد إرسال النموذج، مثل إظهار رسالة نجاح، فستحتاج إلى استخدام Client Component. الحل هو تقسيم المكون إلى جزئين: جزء على السيرفر وجزء على العميل:
// app/login/page.js
export default function LoginPage() {
return (
<div>
<LoginForm />
<LoginStatus />
</div>
);
}
// app/login/LoginForm.js
async function LoginForm() {
async function handleSubmit(formData) {
'use server';
// منطق تسجيل الدخول هنا
}
return (
<form action={handleSubmit}>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">تسجيل الدخول</button>
</form>
);
}
// app/login/LoginStatus.js
'use client';
import { useState } from 'react';
export default function LoginStatus() {
const [status, setStatus] = useState('');
return <div>{status}</div>;
}الـ Caching في Next.js App Router هو سلاح ذو حدين. من جهة، يمكنه تحسين أداء تطبيقك بشكل كبير عن طريق تخزين نتائج الـ fetch والـ Server Components. من جهة أخرى، يمكن أن يؤدي إلى عرض بيانات قديمة للمستخدمين دون أن تدري. المشكلة الأكبر هي أن Next.js يقوم بالتخزين المؤقت تلقائياً في الكثير من الحالات، مما قد يؤدي إلى سلوك غير متوقع. على سبيل المثال، إذا كان لديك مكون يستدعي API للحصول على بيانات ديناميكية:
// app/dashboard/page.js
async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard');
const json = await data.json();
return <div>{json.content}</div>;
}Next.js سيقوم بتخزين نتيجة هذا الـ fetch تلقائياً لمدة 30 ثانية. هذا يعني أنه إذا قام مستخدمان بزيارة الصفحة خلال 30 ثانية، فسيشاهدان نفس البيانات حتى لو تغيرت على السيرفر. المشكلة تصبح أسوأ إذا كنت تستخدم مكونات متداخلة، حيث يمكن أن يتم تخزين كل مكون بشكل مستقل. الحل هو التحكم في الـ Caching بشكل صريح باستخدام الخيارات المتاحة في fetch:
// تعطيل التخزين المؤقت بالكامل
const data = await fetch('https://api.example.com/dashboard', { cache: 'no-store' });
// تحديد مدة التخزين المؤقت
const data = await fetch('https://api.example.com/dashboard', { next: { revalidate: 10 } });لكن حتى هذا ليس كافياً في بعض الحالات. على سبيل المثال، إذا كان لديك مكون يستخدم بيانات من متغير بيئي أو من قاعدة بيانات مباشرة، فإن Next.js لن يقوم بتخزينه تلقائياً، مما قد يؤدي إلى إعادة تنفيذ المكون في كل طلب. الحل هو استخدام الـ Route Segment Config لتحديد سلوك التخزين المؤقت للمكون بأكمله:
// app/dashboard/page.js
export const revalidate = 10; // إعادة التحقق كل 10 ثوانٍ
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard');
const json = await data.json();
return <div>{json.content}</div>;
}المشكلة الأكبر التي واجهتها شخصياً في أحد المشاريع هي عندما استخدمنا مكوناً يستدعي API خارجي يعتمد على الـ IP الخاص بالمستخدم. لأن Next.js يقوم بتخزين نتيجة الـ fetch على مستوى السيرفر، كان جميع المستخدمين يشاهدون نفس البيانات بغض النظر عن موقعهم الجغرافي. الحل كان استخدام dynamic = 'force-dynamic' لتعطيل التخزين المؤقت بالكامل للمكون:
// app/location/page.js
export const dynamic = 'force-dynamic';
export default async function LocationPage() {
const data = await fetch('https://api.example.com/location');
const json = await data.json();
return <div>{json.city}</div>;
}الـ Server Actions هي واحدة من أقوى الميزات في Next.js App Router، لكنها أيضاً واحدة من أكثر الميزات سوء فهماً. الكثير من المطورين يستخدمونها كبديل لـ API Routes، وهذا خطأ شائع. الـ Server Actions مصممة للتفاعلات البسيطة التي تحتاج إلى تنفيذ منطق على السيرفر، مثل إرسال نموذج أو تحديث قاعدة البيانات. المشكلة الأكبر هي أن الكثير من المطورين يحاولون استخدامها لتنفيذ عمليات معقدة، مما يؤدي إلى بطء في الاستجابة وتجربة مستخدم سيئة.
على سبيل المثال، إذا كان لديك نموذج يحتوي على 20 حقلاً وتحتاج إلى التحقق من صحة البيانات قبل إرسالها إلى قاعدة البيانات، فإن استخدام Server Action مباشرة قد يؤدي إلى تجميد الصفحة لمدة ثوانٍ أثناء انتظار الرد من السيرفر. الحل هو تقسيم العملية إلى جزئين: التحقق من الصحة على العميل باستخدام Client Component، ثم إرسال البيانات إلى Server Action:
// app/form/page.js
'use client';
import { useState } from 'react';
import { submitForm } from './actions';
export default function FormPage() {
const [errors, setErrors] = useState({});
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData);
// التحقق من الصحة على العميل
const validati {};
if (!data.email) validationErrors.email = 'البريد الإلكتروني مطلوب';
if (!data.password) validationErrors.password = 'كلمة المرور مطلوبة';
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// إرسال البيانات إلى Server Action
await submitForm(data);
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" />
{errors.email && <p>{errors.email}</p>}
<input type="password" name="password" />
{errors.password && <p>{errors.password}</p>}
<button type="submit">إرسال</button>
</form>
);
}
// app/form/actions.js
'use server';
export async function submitForm(data) {
// منطق إرسال البيانات إلى قاعدة البيانات هنا
}الميزة الحقيقية للـ Server Actions تظهر عندما تستخدمها مع revalidatePath أو revalidateTag لتحديث البيانات المعروضة على الصفحة دون الحاجة إلى إعادة تحميلها بالكامل. على سبيل المثال، إذا كان لديك قائمة مهام وتريد إضافة مهمة جديدة، يمكنك استخدام Server Action لتحديث قاعدة البيانات ثم إعادة التحقق من البيانات المعروضة:
// app/todos/actions.js
'use server';
import { revalidatePath } from 'next/cache';
export async function addTodo(data) {
// إضافة المهمة إلى قاعدة البيانات
await db.todos.create({ data });
// إعادة التحقق من البيانات المعروضة
revalidatePath('/todos');
}هذه التقنية تسمح لك بتحديث واجهة المستخدم فوراً دون الحاجة إلى إعادة تحميل الصفحة أو استخدام حالة محلية معقدة. لكن هناك مشكلة شائعة هنا: إذا كان لديك عدة مكونات على نفس الصفحة تعتمد على نفس البيانات، فإن استخدام revalidatePath سيؤدي إلى إعادة تحميل كل المكونات، مما قد يسبب وميض في واجهة المستخدم. الحل هو استخدام revalidateTag بدلاً من revalidatePath لتحديد بالضبط أي جزء من البيانات يحتاج إلى التحديث:
// app/todos/page.js
async function TodosPage() {
const data = await fetch('https://api.example.com/todos', { next: { tags: ['todos'] } });
const todos = await data.json();
return <TodosList todos={todos} />;
}
// app/todos/actions.js
'use server';
import { revalidateTag } from 'next/cache';
export async function addTodo(data) {
await db.todos.create({ data });
revalidateTag('todos');
}واحدة من أكثر المشاكل خطورة في Next.js App Router هي الـ Memory Leaks التي تحدث في الـ Server Components دون أن يلاحظها المطورون. المشكلة تحدث عندما تحتفظ مكونات السيرفر بمراجع إلى كائنات كبيرة أو بيانات غير ضرورية بعد انتهاء تنفيذ المكون. على سبيل المثال، إذا كان لديك مكون يقرأ ملفاً كبيراً من القرص ويخزنه في متغير:
// app/report/page.js
let cachedData = null;
async function ReportPage() {
if (!cachedData) {
const file = await fs.readFile('./large-report.json');
cachedData = JSON.parse(file);
}
return <Report data={cachedData} />;
}هذا الكود يبدو بريئاً، لكنه في الواقع يؤدي إلى تسرب ذاكرة خطير. المتغير cachedData سيتم الاحتفاظ به في ذاكرة السيرفر بين الطلبات، مما يؤدي إلى زيادة استخدام الذاكرة مع مرور الوقت. المشكلة تصبح أسوأ إذا كان لديك عدة مكونات تفعل نفس الشيء، أو إذا كنت تستخدم مكتبات خارجية تحتفظ بمراجع داخلية. الحل هو تجنب استخدام المتغيرات العامة تماماً في الـ Server Components، واستخدام الـ fetch مع التخزين المؤقت بدلاً من ذلك:
// app/report/page.js
async function ReportPage() {
const data = await fetch('https://api.example.com/report', { next: { revalidate: 3600 } });
const json = await data.json();
return <Report data={json} />;
}إذا كنت بحاجة إلى قراءة ملف من القرص، استخدم مكتبة مثل fs/promises وقم بإغلاق الملف فوراً بعد القراءة. أيضاً، تجنب استخدام مكتبات تحتفظ بحالة داخلية، مثل بعض مكتبات قواعد البيانات أو مكتبات معالجة الصور. إذا كنت مضطراً لاستخدام مكتبة تحتفظ بحالة، فتأكد من تنظيفها بعد الانتهاء:
// app/image/page.js
import { processImage } from 'some-image-library';
async function ImagePage() {
const imageBuffer = await fs.readFile('./image.jpg');
const processedImage = await processImage(imageBuffer);
// تنظيف الموارد
await processImage.cleanup();
return <Image src={processedImage} />;
}المشكلة الأكبر التي واجهتها في أحد المشاريع كانت عندما استخدمنا مكتبة خارجية لمعالجة الصور تحتفظ بمراجع داخلية لكل صورة معالجة. بعد بضعة أيام من التشغيل، بدأ السيرفر في استخدام أكثر من 4 جيجابايت من الذاكرة، وكان الحل الوحيد هو إعادة تشغيل السيرفر كل بضع ساعات. الدرس المستفاد هو: دائماً افحص مكتباتك الخارجية للتأكد من أنها لا تحتفظ بحالة داخلية، وإذا كانت تفعل ذلك، فتأكد من وجود طريقة لتنظيف هذه الحالة بعد الاستخدام.
بعد سنوات من العمل مع Next.js App Router في مشاريع حقيقية، هذه هي القواعد الذهبية التي أستخدمها لضمان أن تطبيقاتي تعمل بسلاسة دون مشاكل:
الـ App Router في Next.js هو أداة قوية جداً، لكنه يتطلب تغييراً في العقلية. إذا كنت لا تزال تفكر فيه كإصدار محسن من Pages Router، فأنت تخسر معظم فوائده. المفتاح هو فهم كيف يعمل خلف الكواليس واستخدام ميزاته بشكل صحيح. ابدأ بمشروع صغير، جرب الأشياء، ارصد الأداء، وعدل بناء على النتائج. بهذه الطريقة، ستكتب تطبيقات لا تعلق أبداً وتوفر تجربة مستخدم ممتازة.