معماری App Router در Next.js برای پروداکشن: مرز Server و Client درست کجاست؟
راهنمای عملی برای جداسازی Server Components، Route Handlers، caching و boundaryهای Client در اپهای واقعی—نه دموهای کوچک.
علی مرتضوی
بنیانگذار پارادایس کد
App Router یک فریمورک UI نیست؛ قرارداد رندر است
بیشتر تیمها App Router را مثل «پوشهبندی جدید برای صفحات» میبینند. در عمل، هر فایل در درخت `app` یک تصمیم رندر، کش و مرز داده است. Server Component پیشفرض یعنی داده نزدیک منبع میماند، HTML اولیه سبکتر میشود، و باندل کلاینت فقط جایی که تعامل لازم است رشد میکند. اگر این قرارداد را جدی نگیرید، خیلی زود همه چیز را با `'use client'` آلوده میکنید و همان بدهی SPA قدیمی را با اسم تازه میسازید.
در پروداکشن، سؤال درست این نیست که «میتوانم این کامپوننت را کلاینتی کنم؟» بلکه «آیا این کامپوننت باید state مرورگر، event listener یا API مرورگر داشته باشد؟» اگر نه، روی سرور بماند. این قانون ساده، معماری را از سلیقه جدا میکند و review را قابل اندازهگیری میکند.
لایهبندی مسیرها: layout، template و segmentهای داده
Layoutها برای shell پایدارند: ناوبری، تم، احراز هویت سطح بالا، و providerهایی که واقعاً باید کل زیردرخت را بپوشانند. Template وقتی معنا دارد که میخواهید روی هر navigation دوباره mount شوید—مثلاً انیمیشن ورود صفحه یا ریست فرم. اشتباه رایج این است که منطق fetch سنگین را داخل layout ریشه بگذارید؛ آنوقت هر تغییر مسیر، هزینه دادهٔ مشترک را دوباره یا بیش از حد نگه میدارد.
Segmentهای مسیر را حول «واحد محصول» بچینید نه حول فایلهای UI. مثلاً `/dashboard/billing` باید مالک داده صورتحساب باشد، نه اینکه هر ویجت جداگانه به API خودش بزند. وقتی مالکیت داده در segment روشن باشد، `loading.tsx` و `error.tsx` هم معنای واقعی پیدا میکنند: اسکلت همان واحد، خطای همان واحد.
Caching عمدی: static، revalidate و بدون کش تصادفی
در App Router، کش تصادفی خطرناکتر از نبود کش است. دادهٔ بازاریابی با `revalidate` طولانی، داشبورد کاربر با کش کوتاه یا بدون کش، و دادهٔ حساس با `no-store`—این سه حالت را صریح نامگذاری کنید. تیمهایی که همه fetchها را یکسان مینویسند، یا stale میفروشند یا origin را میسوزانند.
برای محتوای نیمهپویا، ISR/`revalidateTag` را به رویداد کسبوکار وصل کنید: انتشار مقاله، تغییر قیمت، بهروزرسانی موجودی. Tag باید اسم دامنه داشته باشد (`product:42`)، نه اسم کامپوننت. این کار باعث میشود invalidation از UI جدا شود و چند صفحه همزمان درست تازه شوند.
Route Handlerها را جای «API عمومی محصول» نگذارید مگر واقعاً لازم باشد. برای دادهٔ داخلی صفحه، مستقیم در Server Component بخوانید. Handler را برای webhook، آپلود، یا قرارداد خارجی نگه دارید تا سطح حمله و سطح کش جدا بماند.
Client boundary: کم، مشخص، و نزدیک تعامل
مرز کلاینت را تا جایی پایین ببرید که فقط ویجت تعاملی `'use client'` باشد: مودال، فیلتر زنده، ادیتور، نمودار. والد را سروری نگه دارید تا props اولیه از سرور بیاید و هیدراسیون حداقل شود. الگوی «یک Provider بزرگ دور کل اپ» معمولاً باندل و re-render را بیدلیل بزرگ میکند.
برای state مشترک، اول URL و سرور را امتحان کنید: فیلتر در search params، انتخاب تب در path، دادهٔ اولیه از RSC. فقط وقتی تعامل چندویجتی و کوتاهعمر دارید سراغ context/client store بروید. این ترتیب تصمیم، از دوبارهکاری hydrate و mismatch جلوگیری میکند.
Streaming و Suspense بهعنوان قرارداد UX
Streaming فقط ترفند Lighthouse نیست؛ قرارداد اولویت محتواست. shell و محتوای above-the-fold را زود بفرستید، بلوکهای کند (پیشنهادها، نظرات، ویجتهای جانبی) را پشت Suspense بگذارید. کاربر باید در کمتر از یک ثانیه حس «صفحه آمده» بگیرد، حتی اگر بخشی هنوز در حال resolve باشد.
برای هر boundary یک fallback واقعی طراحی کنید—نه اسپینر عمومی. ارتفاع ثابت یا اسکلت همشکل از CLS جلوگیری میکند. اگر یک بخش همیشه کند است، مشکل معماری داده است نه UI؛ آن را با cache، query جدا، یا endpoint سبکتر حل کنید.
امنیت و مرزهای سرور در App Router
کد سرور در کنار کد کلاینت زندگی میکند؛ پس فرض «فایل دیده نمیشود» کافی نیست. Secret فقط در env سرور، منطق مجوز داخل تابع سرور یا لایه داده، و هر خروجی به کلاینت باید حداقل فیلد لازم را داشته باشد. هرگز آبجکت کامل دیتابیس را به Client Component پاس ندهید.
Server Actions برای mutationهای فرممحور عالیاند، اما باید مثل API امن شوند: اعتبارسنجی ورودی، CSRF/origin check مطابق مدل شما، rate limit، و audit log. Action بدون قرارداد امنیتی فقط RPC پنهان است.
چکلیست معماری قبل از scale
قبل از رشد تیم یا ترافیک، اینها را ثابت کنید: نقشه segmentها و مالک داده، فهرست tagهای کش، قوانین Client boundary در review، و بودجه باندل برای مسیرهای کلیدی. بدون اینها App Router فقط پیچیدگی اضافه میکند.
معماری پایدار یعنی هر مسیر جدید بدون مذاکرهٔ دوباره دربارهٔ «کجا fetch کنیم» ساخته شود. وقتی این قرارداد در تیم جا بیفتد، سرعت فیچر و کیفیت پروداکشن همزمان بالا میرود—و همان چیزی است که در پروژههای جدی Next.js دیده میشود.
سؤالات متداول
آیا باید همه صفحات را Server Component نگه داریم؟
پیشفرض بله. فقط بخشهایی که state مرورگر یا تعامل واقعی دارند کلاینتی شوند؛ بقیه را سروری بگذارید تا باندل و هیدراسیون کنترل شود.
چه زمانی از Route Handler بهجای fetch در RSC استفاده کنیم؟
برای قراردادهای خارجی، webhook، آپلود فایل، یا وقتی کلاینت/سرویس سوم باید HTTP مستقل صدا بزند. برای رندر صفحه، داده را مستقیم در سرور بخوانید.
revalidateTag بهتر است یا revalidate زمانی؟
برای محتوای رویدادمحور tag بهتر است. زمان ثابت وقتی خوب است که منبع تغییر منظم و قابل پیشبینی داشته باشد و stale کوتاه پذیرفتنی باشد.
چطور از Client boundary انفجاری جلوگیری کنیم؟
در PR هر `'use client'` را با دلیل تعامل بررسی کنید، Providerهای سراسری را محدود کنید، و state فیلتر را ترجیحاً در URL نگه دارید.
دانش و مقالات
نیاز به اجرای همین مفاهیم در محصولتان دارید؟
پارادایس کد از مشاوره تا پیادهسازی کامل کنار شماست.