Memo-Note
لیست مطالب
لیست نکات خام
مرور مطالب
NEXTJS

در حال دریافت اطلاعات ...

لیست نکات خام

تکنیک بهینه سازی با new URLSearchParams

گاهی در کلاینت کامپوننت ها می خواید به search params دسترسی داشته باشید می دونیم که باید از useSearchParams استفاده بشه ولی ایرادش اینه با تغییر params کامپوننت شما هم re-render می شه در این وضعیت جایی که فقط نیاز به داشتن یا گرفتن search params دارید بهتره از new URLSearchParams(window.location.search) استفاده بشه که باعث re-rendering نمی شه.

برای type safe شدن params و search params در Page می تونیم به این شیوه عمل کنیم

https://nextjs.org/docs/app/api-reference/file-conventions/page#page-props-helper

چه زمانی از useLinkStatus استفاده کنیم؟

اگر اینترنت کاربر سریع باشد، ممکن است رفتن به صفحه جدید فقط ۵۰ میلی‌ثانیه طول بکشد. اگر شما بلافاصله با کلیک کاربر، علامت لودینگ را نشان دهید، این علامت در یک صدم ثانیه ظاهر و بلافاصله غیب می‌شود. این حالت چشمک‌زدن، تجربه کاربری (UX) بدی دارد و حس پرش یا باگ را به کاربر منتقل می‌کند.

راه‌حل داکیومنت (با استفاده از CSS Animation/Transition):

پیشنهاد می‌دهد که علامت لودینگ در ابتدا نامرئی باشد (opacity: 0) و مثلاً ۱۰۰ میلی‌ثانیه تاخیر (animation-delay: 100ms) برای ظاهر شدن آن تنظیم کنید.

در این حالت دو اتفاق می‌افتد:

۱. اگر اینترنت سریع باشد: صفحه جدید در کمتر از ۱۰۰ میلی‌ثانیه لود می‌شود. کاربر اصلاً علامت لودینگ را نمی‌بیند (چون هنوز در زمان تاخیر است و نامرئی مانده).

۲. اگر اینترنت کند باشد: زمان از ۱۰۰ میلی‌ثانیه می‌گذرد، انیمیشن شروع می‌شود، لودینگ روی صفحه ظاهر می‌شود و کاربر می‌فهمد که سایت در حال پردازش درخواست اوست.

به این تکنیک اصطلاحاً می‌گویند “Debounce کردن لودینگ” تا فقط زمانی نمایش داده شود که واقعاً نیاز است.

نکته: مدیریت وضعیت (State) در URL بدون درگیری سرور (Native History API)

مشکل:

می‌خواهیم وضعیت فعلی کاربر (مثل تب انتخاب شده، مودالِ باز، یا عبارت جستجو) را در آدرس مرورگر (URL) ذخیره کنیم تا لینک قابل بوک‌مارک کردن و اشتراک‌گذاری باشد. اما اگر برای تغییر URL از روتر پیش‌فرض استفاده کنیم، درخواستی به سرور ارسال شده و صفحه مجدداً پردازش/رندر می‌شود، در حالی که ما تمام داده‌ها را سمت کلاینت داریم و این کار باعث افت پرفورمنس و کندی بی‌دلیل می‌شود.

راه‌حل:

استفاده از متدهای بومی مرورگر یعنی window.history.pushState (برای افزودن به تاریخچه) و window.history.replaceState (برای جایگزینی در تاریخچه). این متدها پارامترهای URL را تغییر می‌دهند بدون اینکه صفحه رفرش شود یا درخواستی به سرور برود. در عین حال، فریم‌ورک‌هایی مثل Next.js این تغییر را تشخیص داده و UI را در لحظه آپدیت می‌کنند.

سناریوهای کاربردی:

  1. مدیریت تب‌ها (Tabs):

کاربر در داشبورد خود بین تب‌های «پروفایل» و «تنظیمات» جابه‌جا می‌شود. آدرس مرورگر تغییر می‌کند تا اگر لینک را برای کسی فرستاد، دقیقاً همان تب باز شود، اما جابه‌جایی بین تب‌ها کاملاً سمت کلاینت و بدون بارگذاری مجدد انجام می‌شود.

  1. کنترل مودال‌ها و پاپ‌آپ‌ها:

مودال ورود به سایت باز می‌شود و آدرس تغییر می‌کند. بزرگترین مزیت این کار این است که اگر کاربر در گوشی خود دکمه «بازگشت» (Back) را بزند، به جای اینکه کلاً از صفحه قبل خارج شود، فقط مودال بسته می‌شود.

  1. جستجو و فیلتر زنده (Live Search/Filter):

لیست محصولات از قبل دریافت شده است. کاربر در نوار جستجو تایپ می‌کند و لیست در لحظه فیلتر می‌شود. همزمان آدرس مرورگر آپدیت می‌شود تا وضعیت جستجو در URL ذخیره بماند، بدون اینکه برای هر حرف تایپ شده، سرور درگیر شود.

  1. تغییر حالت نمایش (View Mode):

کاربر نحوه نمایش لیست مقالات را از حالت «جدول» به حالت «کارت» تغییر می‌دهد. این وضعیت در آدرس ذخیره می‌شود تا ترجیح کاربر حفظ شود.

  1. صفحه‌بندی سمت کلاینت (Client-side Pagination):

صد کاربر از سرور دریافت شده‌اند. برای رفتن به صفحه دوم (نمایش ۱۰ کاربر بعدی)، آدرس مرورگر تغییر می‌کند اما چون داده‌ها از قبل موجودند، نیازی به درخواست جدید از سرور نیست.

دلیل افزایش حجم باندل با 'use client' کردن کامپوننت‌های استاتیک

در ری‌اکت، حتی عناصر ظاهر ثابت (مثل تگ‌های عکس یا لینک) کدهای جاوا اسکریپت (JSX) هستند.

  • اگر کل یک ساختار (مثل Layout) را 'use client' کنید، مرورگر مجبور است کدهای جاوا اسکریپتِ سازنده تمام آن عناصر ثابت را دانلود و اجرا کند.

  • اما اگر آن را Server Component نگه دارید، سرور خودش کدها را اجرا کرده و فقط HTML خالص را به مرورگر می‌فرستد (بدون ارسال کد جاوا اسکریپت برای آن بخش).

نتیجه‌گیری: برای سبک ماندن صفحه، دستور 'use client' را فقط به پایین‌ترین سطح ممکن (دقیقاً روی خود کامپوننت‌های تعاملی مثل Search) محدود کنید تا کدهای جاوا اسکریپت اضافی برای بخش‌های استاتیک دانلود نشود.

تکنینک خفن برای اشتراک دیتاها بین کلاینت و سرور با React.Cache

https://nextjs.org/docs/app/getting-started/fetching-data#sharing-data-with-context-and-reactcache

مفهوم Navigation در Next.js

در Next.js به صورت پیش‌فرض رندر شدن صفحات در سمت سرور انجام می‌شود. برای اینکه کاربر منتظر پاسخ سرور نماند و حس کند برنامه بسیار سریع است، Next.js از سه تکنیک اصلی استفاده می‌کند: Prefetching (پیش‌بارگذاری)، Streaming (ارسال تکه‌تکه) و Client-side transitions (انتقال سمت کلاینت).


۱. مسیریابی چگونه کار می‌کند؟ (How navigation works)

برای درک مسیریابی باید ۴ مفهوم را بدانید:

الف) رندر سمت سرور (Server Rendering)

کامپوننت‌ها (مثل Layoutها و Pageها) به طور پیش‌فرض در سرور رندر می‌شوند. این کار به دو زمان تقسیم می‌شود:

  • Prerendering (پیش‌رندر): در زمان Build یا Revalidation انجام و کَش می‌شود.

  • Dynamic Rendering (رندر پویا): دقیقاً در لحظه درخواست کاربر انجام می‌شود.

ب) پیش‌بارگذاری (Prefetching)

داده‌های یک مسیر، قبل از اینکه کاربر روی آن کلیک کند در پس‌زمینه دانلود می‌شود.

  • چطور کار می‌کند؟ به محض اینکه تگ <Link> در صفحه مانیتور (Viewport) کاربر دیده شود، Next.js آن را پیش‌بارگذاری می‌کند.

  • تفاوت مسیر استاتیک و پویا: مسیرهای استاتیک کامل دانلود می‌شوند، اما مسیرهای پویا (Dynamic) یا نادیده گرفته می‌شوند یا فقط تا فایل loading.tsx دانلود می‌شوند تا به سرور فشار نیاید.

ج) استریمینگ (Streaming)

به جای اینکه سرور صبر کند تا کل صفحه پردازش شود، آن را بخش به بخش برای کاربر می‌فرستد.

  • نحوه استفاده: کافیست یک فایل loading.tsx بسازید. Next.js خودش صفحه را درون یک کامپوننت <Suspense> قرار می‌دهد.

د) انتقال سمت کلاینت (Client-side transitions)

وقتی با <Link> به صفحه جدیدی می‌روید، کل صفحه در مرورگر رفرش (Reload) نمی‌شود.

  • بخش‌های مشترک (مثل منوی بالای سایت یا Layout) دست‌نخورده باقی می‌مانند.

  • فقط محتوای صفحه جدید جایگزین می‌شود و اسکرول به بالای صفحه برمی‌گردد.


۲. چه عواملی باعث کندی مسیریابی می‌شوند و راه حل چیست؟ (What can make transitions slow)

گاهی با وجود این بهینه‌سازی‌ها، سایت کند به نظر می‌رسد. دلایل آن شامل موارد زیر است:

الف) مسیرهای پویا بدون loading.tsx

اگر صفحه شما دیتای پویا (مثل سبد خرید) دارد و فایل loading ندارید، کاربر روی لینک کلیک می‌کند و تا زمانی که سرور جواب ندهد، هیچ اتفاقی در صفحه نمی‌افتد (صفحه فریز می‌شود).

  • راه حل: حتماً فایل loading.tsx بسازید تا بلافاصله به کاربر یک اسکلت لودینگ نشان داده شود.

ب) مسیرهای پویا بدون generateStaticParams

اگر صفحات پویایی دارید (مثل مقالات وبلاگ [slug]) که می‌شد از قبل رندر شوند اما این کار را نکرده‌اید، سرور مجبور است در لحظه درخواست آن‌ها را بسازد.

  • راه حل: با تابع generateStaticParams در زمان Build، لیست مقالات را بگیرید تا همه از قبل آماده (Prerender) شوند.

ج) اینترنت کند (Slow networks)

روی اینترنت‌های ضعیف، عملیات Prefetching به موقع تمام نمی‌شود. وقتی کاربر کلیک می‌کند، حتی فایل loading هم هنوز دانلود نشده تا نمایش داده شود.

  • راه حل: استفاده از هوک useLinkStatus. با این هوک می‌توانید به محض اینکه کاربر کلیک کرد، روی خود دکمه یک حالت “در حال انجام…” (Pending) نشان دهید.

  • سناریوی واقعی: کاربر در مترو اینترنت ضعیفی دارد. روی “پرداخت” کلیک می‌کند، چون اتفاقی نمی‌افتد ۳ بار دیگر کلیک می‌کند. با این هوک، با اولین کلیک، رنگ دکمه خاکستری می‌شود تا کاربر بفهمد سیستم در حال تلاش است.

د) غیرفعال کردن پیش‌بارگذاری (Disabling prefetching)

شما می‌توانید با دادن prefetch={false} به تگ <Link>، این قابلیت را خاموش کنید.

  • سناریوی واقعی: شما یک لیست بی‌نهایت (Infinite scroll) مثل تایم‌لاین توییتر دارید که شامل ۱۰۰۰ لینک است. اگر Next.js بخواهد همه را پیش‌بارگذاری کند، اینترنت و سیستم کاربر نابود می‌شود!

  • راه حل جایگزین (تریک): به جای خاموش کردن کامل، کامپوننتی بنویسید که فقط وقتی موس کاربر روی لینک رفت (Hover شد)، پیش‌بارگذاری را انجام دهد (با onMouseEnter).

هـ) تکمیل نشدن هیدراتاسیون (Hydration not completed)

تگ <Link> برای کار کردن به جاوا اسکریپت نیاز دارد. اگر حجم کدهای جاوا اسکریپت سمت کاربر (Client) خیلی زیاد باشد، مرورگر دیرتر آن‌ها را اجرا می‌کند و تا آن زمان Prefetching کار نمی‌کند.

  • راه حل: کاهش حجم باندل (Bundle size) و انتقال منطق‌های پردازشی به سرور.


۳. مثال‌ها: استفاده از API بومی تاریخچه مرورگر (Native History API)

Next.js به شما اجازه می‌دهد URL صفحه را بدون رفرش شدن عوض کنید و پارامترها (مثل ?sort=asc) را آپدیت کنید. این کار با توابع بومی جاوا اسکریپت انجام می‌شود:

الف) window.history.pushState

برای اضافه کردن یک مرحله به تاریخچه (History) مرورگر.

  • سناریوی واقعی: کاربر در یک فروشگاه، محصولات را روی “ارزان‌ترین” مرتب می‌کند. URL به ?sort=asc تغییر می‌کند. اگر دکمه Back (بازگشت) مرورگر را بزند، به حالت بدون فیلتر قبلی برمی‌گردد.

ب) window.history.replaceState

برای جایگزین کردن URL فعلی بدون اضافه شدن به تاریخچه مرورگر (کاربر نمی‌تواند دکمه Back را بزند).

  • سناریوی واقعی: تغییر زبان سایت از انگلیسی به فرانسوی (/en/ به /fr/). در این حالت نمی‌خواهیم کاربر با زدن دکمه Back مرورگر دوباره به زبان انگلیسی برگردد، چون زبان اصلی خودش را انتخاب کرده است. پس وضعیت فعلی را “جایگزین” می‌کنیم.

تمثیل «مهندس برق و ساختمان نوساز»: درک دقیق Hydration و نقش RSC در Next.js

برای درک اینکه چرا ری‌اکت در لود اولیه مرورگر، کدهای HTML را از نو نمی‌سازد (DOM را آپدیتِ کامل نمی‌کند)، فرض کنید در حال ساخت یک ساختمان هوشمند هستیم:

۱. اسکلت و نمای ساختمان (همان HTML اولیه سرور)

وقتی کاربر آدرس سایت را می‌زند، سرور Next.js بلافاصله یک ساختمان کامل با تمام دیوارها، درها، پنجره‌ها و نمای ظاهری می‌سازد و به شهر (مرورگر کاربر) می‌فرستد.

کاربر در کسری از ثانیه ساختمان را می‌بیند. ظاهرش کامل است؛ دکمه‌های آسانسور و کلیدهای برق روی دیوار نصب هستند. اما یک مشکل وجود دارد: ساختمان هنوز برق‌کشی نشده است. اگر کلید برق را بزنید هیچ اتفاقی نمی‌افتد (Non-interactive Preview).

۲. نقشه مهندسی ساختمان (همان RSC Payload)

همزمان با ارسال ساختمان، سرور یک نقشه‌ی بسیار دقیق هم برای مرورگر می‌فرستد. این نقشه به زبان آدمیزاد نیست، بلکه پر از کدهای مهندسی است که می‌گوید: «در طبقه دوم، یک کلید برق (Client Component) داریم که باید این‌طور کار کند و در طبقه اول فقط یک تابلوی نقاشی (Server Component) داریم که اصلاً نیازی به برق ندارد».

۳. ورود مهندس ری‌اکت به صحنه (دانلود JavaScript)

حالا مرورگر فایل‌های جاوا اسکریپت را دانلود می‌کند. این یعنی «مهندس ارشد ری‌اکت» از خواب بیدار می‌شود و به محل ساختمان (مرورگر) می‌رسد.

۴. عملیات برق‌کشی (همان Hydration)

حالا سوال مهم این است: وقتی مهندس ری‌اکت به ساختمان می‌رسد، آیا برای اینکه دکمه‌های برق را فعال کند، ساختمان (DOM) را با بولدوزر خراب می‌کند تا از نو بسازد؟

قطعا خیر!

مهندس ری‌اکت نقشه (RSC Payload) را در یک دست می‌گیرد و وارد ساختمان (HTML رندر شده) می‌شود. او با دقت اتاق‌ها را چک می‌کند:

  • «آها، اینجا یک دکمه لایک (Like Button) هست. طبق نقشه، باید به آن جریان برق وصل کنم.»

  • او فقط سیم‌کشی‌ها را انجام می‌دهد و سنسورها را به دکمه‌ها وصل می‌کند (در برنامه‌نویسی یعنی Attach کردن Event Handlerها مثل onClick به عناصر موجود در DOM).

به این فرآیند که مهندس ری‌اکت به ساختمانِ بی‌جانِ HTML جان می‌بخشد و آن را تعاملی می‌کند، Hydration (هیدراته کردن یا آب‌رسانی) می‌گویند.


⚠️ نکته طلایی (Hydration Error):

چه زمانی مهندس ری‌اکت مجبور به تخریب می‌شود؟

فقط زمانی که بین ساختمان (HTML) و نقشه (RSC Payload) مغایرتی وجود داشته باشد. مثلاً نقشه می‌گوید اینجا باید یک در آبی باشد، اما سرور به اشتباه یک دیوار آجری ساخته است. در این حالت مهندس ری‌اکت گیج می‌شود، ارور می‌دهد (Hydration Mismatch Error) و مجبور می‌شود آن قسمت دیوار را خراب کند و خودش از نو بسازد (Re-render روی کلاینت). اما در یک اپلیکیشن سالم، هیچ تخریبی (Rebuilding DOM) در کار نیست!

مرز کلاینت (Client Boundary)

  • دستور 'use client' یک مرز است: وقتی این دستور را می‌نویسید، به Next.js می‌گویید از این نقطه به بعد، مسئولیت اجرا با مرورگر است.

  • قانون آبشاری: هر فایلی که 'use client' دارد و تمام فایل‌ها و ماژول‌هایی که درون آن import می‌شوند، کلاینتی محسوب می‌شوند.

  • قانون جعبه سیاه: سرور کامپوننت‌های کلاینتی را پردازش نمی‌کند. از نظر سرور، آن‌ها یک “جعبه سیاه” هستند و سرور فقط یک جای‌خالی (Placeholder) برای آن‌ها در RSC Payload می‌گذارد.

۲. چه چیزی وارد باندل جاوااسکریپت (JS Bundle) مرورگر می‌شود؟

  • کامپوننت‌های سروری (بدون 'use client'): کد جاوااسکریپت آن‌ها هرگز به مرورگر نمی‌رود. مرورگر فقط HTML و دیتای ساختاریافته (RSC Payload) آن‌ها را دریافت می‌کند.

  • کامپوننت‌های کلاینتی (دارای 'use client'): تمام کدهای جاوااسکریپت آن‌ها برای مرورگر ارسال می‌شود تا بتوانند تعاملی (Hydrate) شوند.

  • کامپوننت‌های مشترک (مثل یک دکمه یا اسپینر بدون 'use client'):

  • اگر در یک فایل سروری ایمپورت شوند -> سروری می‌مانند (JS ارسال نمی‌شود).

  • اگر در یک فایل کلاینتی ایمپورت شوند -> کلاینتی می‌شوند (JS آن‌ها به باندل مرورگر اضافه می‌شود).

۳. الگوی ترکیب (Children / Component as Prop Pattern)

برای جلوگیری از کلاینتی شدن کامپوننت‌های سروری و افزایش حجم باندل مرورگر، از این الگو استفاده می‌شود:

  • ❌ روش اشتباه (باعث سنگین شدن باندل کلاینت می‌شود):

ایمپورت کردن مستقیم یک کامپوننت سروری (مثل Spinner) داخل فایلِ یک کامپوننت کلاینتی (مثل Button).

  • ✅ روش درست (حفظ صفر بایت جاوااسکریپت برای کامپوننت سروری):

کامپوننت کلاینت (Button) را طوری بنویسید که یک prop (مثل children) دریافت کند. سپس در یک فایل سروری (Page)، هر دو را ایمپورت کرده و کامپوننت سروری را درون کامپوننت کلاینتی قرار دهید: <Button> <Spinner /> </Button>.

💡 فرمول کلی برای به یاد سپردن:

“ایمپورتِ” فایل در کلاینت = تبدیل شدن به کلاینت و ارسال JS.

“پاس دادن به عنوان Children” از سرور به کلاینت = باقی ماندن در سرور و ارسال نشدن JS.

ارسال سرور کامپوننت به کلاینت کامپوننت از طریق Children Props

۱. مفهوم این متن چیست؟

به طور خلاصه، در Next.js شما نمی‌توانید یک Server Component را مستقیماً داخل یک Client Component ایمپورت (Import) کنید. اما می‌توانید یک Server Component را به عنوان یک پراپ (معمولاً پراپ children) از طریق یک کامپوننت پدر (که روی سرور است) به یک Client Component پاس بدهید.

در مثال داکیومنت، <Modal> نیاز به تعامل کاربر دارد (مثلاً با کلیک باز و بسته شود)، پس باید use client باشد. اما <Cart> (سبد خرید) باید اطلاعات را از دیتابیس بخواند، پس بهتر است Server Component باشد. راه حل این است که در صفحه اصلی (Page)، سبد خرید را به عنوان فرزند (children) به مودال پاس بدهیم.


۲. چرا به چنین کاری نیاز داریم؟

  • حفظ امنیت و کارایی سرور: اگر <Cart> را تبدیل به Client Component می‌کردیم، مجبور بودیم تمام لاجیک گرفتن داده‌ها (Data Fetching) را سمت مرورگر ببریم که هم نیاز به ساخت API داشت و هم ممکن بود کلیدهای امنیتی لو بروند.

  • کاهش حجم باندل جاوااسکریپت (Bundle Size): با این الگو، کدهای سنگین (مثل اتصال به دیتابیس یا کتابخانه‌های سنگین پردازش داده) که در <Cart> هستند، سمت سرور می‌مانند و به مرورگر کاربر ارسال نمی‌شوند. کاربر فقط کدهای JS مربوط به <Modal> را دانلود می‌کند.

  • جلوگیری از Client-side شدن کل اپلیکیشن: اگر این الگو نبود، به محض اینکه یک کامپوننت والد (مثل Layout یا Modal) نیاز به useState داشت، مجبور می‌شدیم تمام فرزندان آن را هم کلاینت کنیم که کل مزیت Server Components از بین می‌رفت.


۳. چند سناریوی دیگر برای استفاده از این الگو

  1. آکاردئون‌ها (Accordion) یا منوهای بازشو: خود آکاردئون برای باز و بسته شدن نیاز به use client دارد. اما محتوای داخل هر بخش آکاردئون می‌تواند یک مقاله سنگین خوانده شده از CMS باشد که به عنوان children (از نوع Server) به آن پاس داده می‌شود.

  2. اسلایدرها و کاروسل‌ها (Carousel): لاجیک کشیدن (Swipe)، دکمه‌های Next/Prev و انیمیشن‌ها کلاینت هستند. اما خود اسلایدها (تصاویر و متن‌هایی که از دیتابیس می‌آیند) روی سرور رندر شده و به اسلایدر پاس داده می‌شوند.

  3. نقشه تعاملی (Interactive Map): نقشه (Google Maps یا Leaflet) یک Client Component است که کاربر روی آن زوم می‌کند. اما مارکرها (Markers) یا پاپ‌آپ‌های روی نقشه می‌توانند کامپوننت‌های سرور باشند که اطلاعاتشان را مستقیماً از دیتابیس خوانده‌اند.


۴. کجا بهتره استفاده بشه؟

هر زمان که یک پوسته یا ظرف (Container) تعاملی دارید که قرار است داده‌های ثابت یا مبتنی بر سرور (Data-driven) را درون خود نمایش دهد. به طور کلی، هر وقت خواستید دور یک دیتای سروری، یک ویژگی تعاملی (مثل کلیک، هاور، انیمیشن) بپیچید، این بهترین الگوی ممکن است.


۵. روال رندرینگ (Rendering Flow) دقیقاً چطور پیش می‌رود؟

روال رندرینگ در این الگو بسیار جالب و هوشمندانه است (همان‌طور که در پاراگراف آخر داکیومنت نوشته شده):

  1. رندر روی سرور: وقتی کاربر درخواستی می‌فرستد، Next.js شروع به رندر کردن کامپوننت <Page> روی سرور می‌کند.

  2. تشخیص کلاینت و سرور: به <Modal> می‌رسد. می‌بیند use client است. پس می‌گوید: “من کدهای این را اجرا نمی‌کنم، به جایش یک جای خالی (Placeholder) می‌گذارم تا مرورگر بعداً آن را بسازد.”

  3. رندر کامل فرزندان سروری: سپس به <Cart> می‌رسد (که به عنوان children پاس داده شده). با اینکه <Cart> داخل یک کامپوننت کلاینت است، Next.js آن را به طور کامل روی سرور رندر می‌کند (دیتا را می‌گیرد و HTML آن را می‌سازد).

  4. ساخت Payload: سرور یک خروجی به نام RSC Payload (ساختار درختی React) می‌سازد. در این خروجی، <Cart> کامل ساخته شده، اما <Modal> فقط یک رفرنس به فایل جاوااسکریپت است.

  5. ارسال به مرورگر (Client): مرورگر این درخت را دریافت می‌کند. HTML سبد خرید را فوراً نمایش می‌دهد. سپس جاوااسکریپت <Modal> را دانلود و اجرا می‌کند (عملیات Hydration).

  6. نتیجه نهایی: حالا کاربر یک مودال دارد که باز و بسته می‌شود، و محتوای داخلش (سبد خرید) از قبل روی سرور ساخته و آماده شده است و نیازی به لودینگ مجدد دیتا سمت کلاینت ندارد.

مهم:

اجازه بدهید این تناقض ظاهری را برطرف کنیم. کلید حل این معما، درک تفاوت بین Server Components و SSR (Server-Side Rendering) است.

وقتی می‌گوییم "use client" یک مرز (Boundary) است، منظورمان این نیست که سرور تولید HTML آن را متوقف می‌کند.

اتفاقی که می‌افتد این است:

۱. تفاوت Client Component با کامپوننت‌های سنتی (SPA): در یک برنامه خالص React (مثل Create React App)، کلاینت کامپوننت‌ها واقعاً روی سرور رندر نمی‌شوند و مرورگر یک تگ خالی <div id="root"> می‌گیرد. اما Next.js اینطور کار نمی‌کند.

۲. SSR برای همه است: در Next.js، در بارگذاری اولیه صفحه، سرور هم Server Componentها و هم Client Componentها را یک بار اجرا می‌کند تا HTML اولیه (Pre-rendered HTML) را بسازد. چرا؟ برای اینکه کاربر صفحه سفید نبیند و سئو (SEO) عالی باشد.

۳. پس مرز (Boundary) یعنی چه؟

وقتی Next.js به "use client" می‌رسد می‌گوید: “این کامپوننت و بچه‌هایش به جاوااسکریپت در مرورگر نیاز دارند (چون useState، useEffect یا onClick دارند). پس من علاوه بر اینکه HTML اولیه آن‌ها را می‌سازم، کدهای جاوااسکریپت (JS Bundle) آن‌ها را هم بسته‌بندی می‌کنم و به مرورگر می‌فرستم تا بعداً فعال (Hydrate) شوند.”

در مقابل، برای Server Componentها هیچ جاوااسکریپتی به مرورگر نمی‌فرستد.

در مورد مثال <Modal> و <Cart>:

سرور وقتی در حال ساخت HTML است، کد <Modal> (که Client است) را می‌بیند. وضعیت اولیه‌اش را می‌خواند (مثلاً isOpen: false). بر اساس این وضعیت، HTML آن را تولید می‌کند. چون مودال بسته است، محتوای <Cart> (که یک Server Component است و از قبل روی سرور پردازش شده و به عنوان children به مودال پاس داده شده) در HTML نهایی طوری قرار می‌گیرد که دیده نشود (یا اصلاً در DOM قرار نمی‌گیرد، بسته به کد شما).

خلاصه:

سرور بی‌خیالِ کامپوننت‌های کلاینت نمی‌شود. سرور HTML آن‌ها را می‌سازد، اما منطق تعاملی آن‌ها (مثل کلیک کردن) را به عنوان یک وظیفه برای مرورگر (کلاینت) کنار می‌گذارد. به همین دلیل هیچ پرشی (Flicker) اتفاق نمی‌افتد.

ابهام و نکته ظریف دو نکته قبلی

باید بدانیم که Next.js در سمت سرور، صفحه را در دو مرحله مجزا آماده می‌کند:

مرحله ۱: تولید ساختار داده (RSC Payload) -> اینجاست که قانون «جعبه سیاه» صدق می‌کند

در این مرحله، React در حال ساخت یک نقشه درختی (JSON-مانند) از اجزای سایت شماست.

  • وقتی به Server Componentها می‌رسد، آن‌ها را اجرا می‌کند و دیتا را داخل این نقشه می‌گذارد.

  • وقتی به مرز 'use client' می‌رسد، متوقف می‌شود. در این مرحله، React می‌گوید: “این یک جعبه سیاه است. من کدهای درون این کامپوننت را اجرا نمی‌کنم، فقط یک جای‌خالی (Placeholder) در نقشه می‌گذارم تا مرورگر بداند بعداً باید فایل JS این بخش را دانلود و اجرا کند.”

در پیامی که دیروز به شما دادم، دقیقاً داشتم این مرحله (معماری React Server Components) را توضیح می‌دادم.

مرحله ۲: تولید HTML (مرحله SSR) -> اینجاست که جلوی پرش (Flicker) گرفته می‌شود

اگر Next.js همان خروجی مرحله قبل را به مرورگر بفرستد، کاربر یک صفحه سفید می‌بیند تا زمانی که جاوااسکریپت لود شود.

بنابراین، Next.js یک مرحله دیگر روی سرور انجام می‌دهد: آن نقشه (RSC Payload) را با وضعیت اولیه (Initial State) کلاینت کامپوننت‌ها ترکیب می‌کند و یک رشته متنی HTML ثابت می‌سازد. در این مرحله، سرور به کدهای کلاینتی نگاه می‌کند تا فقط بداند چه HTML اولیه‌ای بسازد (بدون اینکه منطق تعاملی مثل onClick را فعال کند).

خلاصه:

  • تناقضی وجود ندارد، فقط مفاهیم با هم قاطی شده‌اند.

  • کلاینت کامپوننت‌ها از نظر معماری دیتای React (مرحله ۱) یک جعبه سیاه هستند و منطق آن‌ها هرگز جزو کدهای سروری محسوب نمی‌شود (کد جاوااسکریپتشان به مرورگر می‌رود).

  • اما همان کلاینت کامپوننت‌ها در لحظه آخر (مرحله ۲) توسط سرور به HTML تبدیل می‌شوند (SSR می‌شوند) تا رابط کاربری بدون پرش (Flicker) و به صورت کامل به کاربر نمایش داده شود.

به زبان ساده در دنیای Next.js:

کلاینت کامپوننت‌ها “Server Component” نیستند، اما روی سرور “SSR” می‌شوند.

دقت شما در پیدا کردن این تضادِ ظاهری بسیار عالی و کاملاً بجا بود!

نکته فوق مهم

Good to know: You should render providers as deep as possible in the tree – notice how ThemeProvider only wraps {children} instead of the entire <html> document. This makes it easier for Next.js to optimize the static parts of your Server Components.

مسئله Environment Poisoning

بیایید این متن را بخش به بخش و دقیق بشکافیم:

۱. صورت مسئله: خطر اشتراک کد بین سرور و کلاینت

در Next.js، شما معمولاً فایل‌هایی دارید که شامل توابع کمکی هستند (مثلاً فایلی به نام lib/data.ts برای دریافت داده‌ها).

مشکل اینجاست که در جاوااسکریپت، هم کامپوننت‌های سمت سرور (Server Components) و هم کامپوننت‌های کلاینت (Client Components) می‌توانند این فایل را import کنند.

اگر تابعی درون این فایل باشد که از یک متغیر محیطیِ محرمانه (مثل process.env.API_KEY) استفاده کند و شما به اشتباه آن تابع را درون یک کامپوننت کلاینت (که کد آن به مرورگر کاربر می‌رود) ایمپورت کنید، خطر لو رفتن آن کلید (API Key) وجود دارد.

۲. مکانیزم دفاعی پیش‌فرض Next.js (جلوگیری از لو رفتن)

Next.js برای جلوگیری از این فاجعه امنیتی یک قانون سخت‌گیرانه دارد:

  • فقط متغیرهای محیطی که پیشوند NEXT_PUBLIC_ دارند اجازه دارند به مرورگر (Client bundle) ارسال شوند.

  • اگر متغیری این پیشوند را نداشته باشد (مثل همان API_KEY در مثال)، Next.js به جای لو دادن آن، مقدار آن را با یک رشته خالی (Empty String "") جایگزین می‌کند.

نتیجه این مکانیزم: کلید شما لو نمی‌رود (امنیت حفظ می‌شود)، اما اگر آن تابع در کلاینت اجرا شود، به درستی کار نخواهد کرد (چون کلید API خالی است و درخواست شما با خطا مواجه می‌شود). پیدا کردن دلیل این باگ برای برنامه‌نویس می‌تواند بسیار گیج‌کننده باشد.

۳. راه‌حل اصولی: استفاده از پکیج server-only

برای جلوگیری از این خطاهای گیج‌کننده و اطمینان از اینکه کدهای مخصوص سرور هرگز در کلاینت استفاده نمی‌شوند، Next.js راه‌حلی به نام server-only معرفی کرده است.

شما عبارت import 'server-only' را در بالاترین خط فایل lib/data.js قرار می‌دهید.

اتفاقی که می‌افتد: از این به بعد، اگر یک برنامه‌نویس به اشتباه تلاش کند این فایل را در یک Client Component (فایلی که 'use client' دارد) ایمپورت کند، Next.js برنامه را در همان مرحله Build (ساخت) متوقف می‌کند و یک ارور کاملاً واضح می‌دهد. این یعنی جلوگیری از خطا قبل از وقوع آن.

۴. روی دیگر سکه: پکیج client-only

همان‌طور که کدهای سرور نباید در کلاینت اجرا شوند، برخی کدها هم فقط مخصوص مرورگر هستند (مثلاً کدهایی که از شیء window یا localStorage استفاده می‌کنند). اگر این کدها در سرور اجرا شوند، ارور می‌دهند.

برای این موارد، می‌توانید از import 'client-only' در فایل‌های جاوااسکریپتی خود استفاده کنید تا اگر کسی به اشتباه آن‌ها را در Server Component ایمپورت کرد، در زمان Build ارور دریافت کند.

۵. نکات فنی درباره نصب این پکیج‌ها

  • نصب این پکیج‌ها از طریق npm/yarn/pnpm (مثلاً pnpm add server-only) از نظر فنی اختیاری است. خود Next.js به صورت داخلی وقتی عبارت import 'server-only' را می‌بیند، منطق ارور دادن را اجرا می‌کند.

  • پس چرا نصبشان کنیم؟ نصب آن‌ها فقط برای این است که ابزارهای بررسی کد (مثل ESLint یا TypeScript) خطای “پیدا نشدن پکیج” ندهند. محتوای واقعی این پکیج‌ها روی NPM عملاً خالی است و کار اصلی را خود موتور Next.js انجام می‌دهد.

خلاصه کل متن: برای جلوگیری از اجرای اشتباهِ کدهای سرور (که حاوی اطلاعات حساس هستند) در مرورگر، بالای فایل‌های حساس بنویسید import 'server-only'. این کار باعث می‌شود اگر اشتباهی رخ داد، برنامه بلافاصله با ارور واضح متوقف شود، نه اینکه در سکوت خراب شود.

مفهوم Memoization

ریشه Memoization از کلمه Memorandum (یادداشت کردن برای به خاطر سپردن) می‌آید و شبیه به کلمه Memorize است. در برنامه‌نویسی یعنی: “جواب را به خاطر بسپار تا مجبور نشوی دوباره همان کار را تکرار کنی.”

نحوه رفتار و تفاوت آن با کش (Cache):

بله، این مفهوم نوعی کش کردن است، اما بسیار کوتاه‌مدت. تفاوت اصلی اینجاست:

  • کش معمولی (Caching): داده‌ها را برای مدت طولانی ذخیره می‌کند. مثلاً اگر کاربر A صفحه را باز کند، سرور داده را می‌گیرد و کش می‌کند. ۵ دقیقه بعد کاربر B می‌آید و سرور همان داده قبلی را به او می‌دهد.

  • مموایز شدن (Memoization): فقط و فقط در طول یک چرخه رندر (یک بار لود صفحه توسط یک کاربر) زنده است.

دقیقاً چطور کار می‌کند؟

فرض کنید یک کاربر وارد صفحه پروفایل می‌شود. سرور شروع به ساختن (رندر) صفحه می‌کند.

درون این صفحه ۳ کامپوننت مختلف (مثلاً هدر، سایدبار و بدنه اصلی) وجود دارند که هر سه به اطلاعات کاربر نیاز دارند و هر سه در کدهایشان نوشته شده:

fetch('https://api.com/user/1')

اتفاقی که می‌افتد این است:

۱. کامپوننت اول fetch را اجرا می‌کند. سرور درخواست را به API می‌فرستد. زمان می‌برد تا جواب بیاید.

۲. وقتی جواب آمد، سرور آن را موقتاً یادداشت می‌کند (Memoize).

۳. کامپوننت‌های دوم و سوم که به همان خط fetch می‌رسند، سرور دیگر درخواست جدیدی به API نمی‌فرستد! بلکه همان جوابِ یادداشت شده را به آن‌ها می‌دهد.

نتیجه: به جای ۳ درخواست شبکه، فقط ۱ درخواست انجام شد.

اما به محض اینکه صفحه ساخته شد و به مرورگر کاربر ارسال شد، آن یادداشت پاره می‌شود. اگر کاربر صفحه را رفرش کند، کل این پروسه از نو تکرار می‌شود.

به همین دلیل به شما اجازه می‌دهد بدون نگرانی از افت سرعت، fetch را در هر کامپوننتی که به آن نیاز دارید بنویسید و نگران تکراری بودن درخواست‌ها نباشید.

نکته مهم در مورد Loading.js و Suspense

به طور کلی وقتی loading.js اضافه می شه کل page.tsx شما رپ می شه داخل یک Suspense به شکل اتوماتیک و fallback اش می شه فایل loading.tsx پس اینجا چند تا نکته مهم پیش میاد اگه پیج شما در چند جای مختلف به یه سری کامپوننت dependecy داشته باشه پیج به اندازه کندترین اون طول می کشه تا لود بشه و stream بشه و نمایش داده بشه در این حالت بهتره از suspense به شکل دستی جایی که می خوایم و نیاز داریم استفاده کنیم. همینطور اگه layout شما به چیزی وابسته بشه مثل api یا ... اینجا دیگه loading.js کمکی نمی کنه

نکته مهم این متن اینه:
مشکل کندترین درخواست: چون مرز <Suspense> در بالاترین سطح صفحه است، تا زمانی که کُندترین کامپوننت سروری (کندترین fetch) آماده نشود، کاربر فقط صفحه لودینگ را می‌بیند. استفاده دستی از <Suspense> اجازه می‌دهد بخش‌های سریع‌تر بلافاصله استریم و نمایش داده شوند.

سه روش مهم فچ در نکست و مزیت ها

برای یادداشت‌برداری، تفاوت و جایگاه این سه روش را به صورت خلاصه و دسته‌بندی‌شده در ادامه آورده‌ام:

1. روش Suspense با کامپوننت واسط (Async Server Component)

این روش استاندارد و پیش‌فرض Next.js (App Router) است.

  • جایگاه (محل اجرا): کاملاً سمت سرور.

  • نحوه فچ: سرور درخواست را می‌فرستد، await می‌کند و بعد از دریافت دیتا، HTML نهایی را می‌سازد. در این حین Suspense پوسته لودینگ را به کاربر نشان می‌دهد.

  • سرعت اجرا: بالاترین سرعت در بارگذاری اولیه (Initial Load). چون مرورگر کلاینت هیچ درخواست شبکه‌ای برای دیتا نمی‌فرستد و مستقیماً HTML آماده را دریافت می‌کند.

  • مزیت اصلی: بهترین انتخاب برای SEO. امنیت بالا (کدهای فچ و API Key ها در سرور می‌مانند). عدم درگیری منابع مرورگر کاربر.

2. روش پاس دادن Promise و استفاده از use

این روش برای استریم کردن دیتا از سرور به یک Client Component بدون متوقف کردن سرور است.

  • جایگاه (محل اجرا): فچ در سرور شروع می‌شود (بدون await)، اما نتیجه در کلاینت (توسط هوک use) خوانده و مصرف می‌شود.

  • نحوه فچ: سرور یک Promise می‌سازد و آن را به عنوان Prop به کلاینت می‌فرستد. کلاینت کامپوننت با خواندن آن توسط use متوقف (Suspend) می‌شود تا دیتا استریم شود.

  • سرعت اجرا: زمان رسیدن به اولین بایت (TTFB) بسیار سریع است چون سرور منتظر دیتا نمی‌ماند. کاربر بلافاصله بقیه صفحه را می‌بیند.

  • مزیت اصلی: حذف کامپوننت‌های واسط (Wrapper). بسیار عالی برای زمانی که نمی‌خواهید برای یک فچ کوچک، یک فایل Server Component جداگانه بسازید یا زمانی که کامپوننت والد خودش از نوع Client است.

3. روش استفاده از کتابخانه‌های کلاینت (مثل React Query یا SWR)

این روش برای مدیریت دیتای پویا و تعاملی در مرورگر است.

  • جایگاه (محل اجرا): کاملاً سمت کلاینت (مرورگر کاربر).

  • نحوه فچ: مرورگر ابتدا کدهای جاوااسکریپت و HTML اولیه را دانلود می‌کند، سپس کامپوننت Mount می‌شود و تازه از داخل مرورگر یک ریکوئست شبکه (مثلاً به API بک‌اند) ارسال می‌شود.

  • سرعت اجرا: بارگذاری اولیه (First Paint) کندتر است، چون کاربر باید منتظر رفت و برگشت درخواست از مرورگر بماند (آبشار کلاینت). اما در تعاملات بعدی و جابجایی بین صفحات، به دلیل وجود سیستم Caching بسیار سریع است.

  • مزیت اصلی: قدرتمندترین روش برای مدیریت چرخه حیات دیتا. دارای قابلیت‌های بی‌نظیر مثل: کش کردن دیتا در مرورگر، Revalidation خودکار (مثلاً هنگام فوکوس روی تب مرورگر)، مدیریت آپدیت‌ها (Mutations) و همگام‌سازی پس‌زمینه. ایده‌آل برای داشبوردهای کاربری و دیتای خصوصی/پویا.


خلاصه برای تصمیم‌گیری (TL;DR):

  • دیتای اصلی و سئو محور صفحه: روش 111 (Server Component + Suspense)

  • نیاز به استریم مستقیم یک بخش به کلاینت بدون کامپوننت واسط: روش 222 (Promise + use)

  • دیتای تعاملی، داشبورد کاربری و نیاز به کش/آپدیت مداوم: روش 333 (React Query)



دقت کنید مزیت های روش دوم نسبت به روش اول اغراق نشه:
نمی‌توان گفت روش اول هیچ مزیتی ندارد. اگرچه در ظاهر روش دوم همان کار را با کُد کمتر (بدون کامپوننت واسط) انجام می‌دهد، اما تفاوت‌های معماری مهمی دارند که باعث می‌شود روش اول (Async Server Component) همچنان روش استاندارد و ارجح در Next.js باشد.

مزایای روش اول نسبت به روش دوم:

111. پردازش در سرور و کاهش حجم باندل (Bundle Size):

در روش اول، کامپوننت واسط یک Server Component است. شما می‌توانید داده‌ها را پس از await کردن، فیلتر کنید، تغییر شکل دهید و فقط بخش مورد نیاز را به Client Component پاس بدهید. تمام کتابخانه‌هایی که برای این پردازش استفاده می‌کنید در سرور می‌مانند و به مرورگر کاربر ارسال نمی‌شوند.

222. سادگی و خوانایی کد:

استفاده از await یک استاندارد جاوااسکریپتی جاافتاده، خوانا و قابل پیش‌بینی است. دیباگ کردن دیتای آماده در سرور بسیار راحت‌تر از پیگیری یک Promise در حال استریم بین سرور و کلاینت است.

333. محدودیت‌های انتقال داده (Serialization):

پاس دادن دیتای خام و حل‌شده (مثل یک آبجکت JSON) از سرور به کلاینت (روش اول) بسیار امن‌تر و پایدارتر است. پاس دادن Promise از مرز سرور به کلاینت (روش دوم) پیچیدگی‌های خاص خودش را دارد و Next.js باید آن را سریالایز کند.

نتیجه:

الگوی پیش‌فرض و توصیه شده معماری App Router همان روش اول است. روش دوم (Promise + use) بیشتر به عنوان یک “ابزار فرار” (Escape Hatch) برای مواقعی طراحی شده که ساختار درختی کامپوننت‌های شما اجازه ساخت کامپوننت واسط سروری را نمی‌دهد (مثلاً وقتی کامپوننت والد خودش کلاینت است).

نکته مهم: تفاوت فراخوانی Server Action با await مستقیم در برابر startTransition

هنگام استفاده از Server Actionها در کلاینت کامپوننت‌ها، بسته به نیازتان یکی از دو روش زیر را انتخاب کنید:

۱. فراخوانی مستقیم با await (بدون Transition)

  • کاربرد: عملیات‌های ساده، سریع و نقطه‌ای که فقط یک State محلی (useState) را در همان کامپوننت تغییر می‌دهند (مثل آپدیت کردن عدد لایک).

  • ویژگی: مثل یک درخواست API معمولی عمل می‌کند. وضعیت بارگذاری (Loading) را باید دستی مدیریت کنید و ممکن است در آپدیت‌های سنگین، رابط کاربری لحظه‌ای فریز شود.

۲. فراخوانی داخل startTransition (یا استفاده از useTransition / useActionState)

  • کاربرد حیاتی: زمانی که Server Action شما شامل دستورات روتر Next.js مثل revalidatePath، revalidateTag یا redirect است.

  • ویژگی: به React می‌گوید رابط کاربری را قفل نکند تا صفحه همچنان واکنش‌گرا بماند. متغیر isPending را برای ساخت راحت‌ترِ Loading به شما می‌دهد و رندر مجدد صفحه را نرم‌تر انجام می‌دهد.

📌 نکته طلایی فرم‌ها: اگر Server Action را مستقیماً به پراپ action در تگ <form> بدهید، React و Next.js پشت صحنه به طور خودکار آن را درون یک Transition اجرا می‌کنند و نیازی به نوشتن دستی startTransition نیست.

نکته مهم: مدیریت مقادیر غیرقطعی (مثل زمان یا عدد تصادفی) در رندرینگ سرور Next.js

داستان چیه؟ (The Problem)

Next.js برای افزایش سرعت، به صورت پیش‌فرض کامپوننت‌ها را در زمان بیلد یا در اولین درخواست، رندر و نتیجه‌ی آن را کش می‌کند (Static Rendering). اگر در کامپوننتی از توابع غیرقطعی مثل Date.now() یا Math.random() استفاده کنید، مقدار آن‌ها فقط یک بار در زمان رندر اولیه محاسبه شده و در خروجی HTML “فریز” یا “منجمد” می‌شود. در نتیجه، تمام کاربران بعدی همان مقدار قدیمی و کش‌شده را می‌بینند، نه یک مقدار جدید.

چرا به راهکار نیاز داریم؟ (The Use Cases)

دو سناریوی کاملاً متفاوت وجود دارد که باید مدیریت شوند:

  1. نیاز به مقدار جدید در هر درخواست: می‌خواهیم بخشی از صفحه همیشه به‌روز باشد. مثلاً:

  • نمایش زمان دقیق ورود کاربر به صفحه.

  • تولید یک شناسه‌ی یکتای درخواست (Request ID) برای لاگ‌ها.

  • نمایش یک بنر تبلیغاتی تصادفی در هر بار بازدید از صفحه.

  1. نیاز به مقدار ثابت در طول عمر کش: می‌خواهیم یک مقدار تصادفی تولید شود، اما برای مدتی (مثلاً ۲۴ ساعت) برای همه کاربران ثابت بماند. مثلاً:

  • نمایش “محصول پیشنهادی روز” که به صورت تصادفی انتخاب می‌شود اما تا فردا تغییر نمی‌کند.

  • اجرای تست A/B که کاربر را به صورت تصادفی در یک گروه قرار می‌دهد و این گروه تا پایان تست ثابت می‌ماند.

راهکار چیه؟ (The Solution)

Next.js برای هر کدام از این سناریوها یک راهکار مشخص ارائه می‌دهد:

  • راهکار ۱ (برای مقدار جدید در هر درخواست):

  • باید رندر آن بخش از صفحه را به زمان درخواست (Request Time) منتقل کنیم.

  • روش: قبل از فراخوانی تابع غیرقطعی، از await connection() استفاده کنید و کامپوننت را درون یک <Suspense> قرار دهید.

  • نتیجه: این کار به Next.js می‌گوید که این کامپوننت داینامیک است و باید منتظر بماند. با استفاده از PPR، پوسته ثابت صفحه فوراً ارسال می‌شود و محتوای این بخش داینامیک پس از آماده شدن، به کلاینت استریم می‌شود.

  • راهکار ۲ (برای مقدار ثابت در طول عمر کش):

  • باید نتیجه‌ی اولین اجرای تابع غیرقطعی را در کش سرور ذخیره کنیم.

  • روش: با استفاده از use cache

  • نتیجه: در اولین درخواست، تابع اجرا شده و مقدار تصادفی تولید و کش می‌شود. در درخواست‌های بعدی تا زمان انقضای کش، همان مقدار کش‌شده برگردانده می‌شود.

نکته: نحوه و الگوریتم رندرینگ جدید Next.js با use cache

خیلی مهم و جذاب:
https://nextjs.org/docs/app/getting-started/caching#how-rendering-works

نکته مهم: مفهوم cache life

تعیین می‌کند داده‌ای که داخل 'use cache' کش شده:

  • چه مدت تازه (fresh) است

  • چه مدت قدیمی ولی قابل استفاده (stale) است

  • چه زمانی باید دوباره fetch شود (revalidate)

  • چه زمانی کاملاً منقضی شود (expire)

سه زمان اصلی

زمان اول: stale

مدتی که بعد از آن داده دیگر تازه محسوب نمی‌شود.

اما:

  • هنوز از cache به کاربر داده می‌شود

  • فقط به سیستم می‌گوید داده کمی قدیمی شده

زمان دوم: revalidate

زمانی که بعد از آن:

  • درخواست جدید بیاید

  • کاربر هنوز cache را می‌گیرد

  • ولی در background داده دوباره fetch می‌شود

  • cache با داده جدید جایگزین می‌شود

این همان الگوی stale‑while‑revalidate است.

زمان سوم: expire

زمانی که:

  • cache دیگر قابل استفاده نیست

  • حذف می‌شود

  • درخواست بعدی باید منتظر fetch واقعی بماند

مثال

stale: 5m

revalidate: 1h

expire: 1d

رفتار:

داده fresh است

از cache داده می‌شود

5 دقیقه → 1 ساعت

داده stale است

ولی هنوز از cache داده می‌شود

بعد از 1 ساعت

درخواست می‌آید

cache سریع داده می‌شود

همزمان در background داده دوباره fetch می‌شود

بعد از 1 روز

cache منقضی می‌شود

درخواست بعدی باید fetch واقعی انجام دهد

چرا این سیستم وجود دارد

هدف:

  • پاسخ سریع به کاربر (cache)

  • کاهش فشار به دیتابیس

  • آپدیت شدن تدریجی داده در background

الگو: Stale‑While‑Revalidate

تفاوت revalidateTag و updateTag

در سیستم کش جدید Next.js می‌توان داده‌های کش‌شده را برچسب‌گذاری (tag) کرد تا بعداً بتوان آن‌ها را به‌صورت هدفمند نامعتبر کرد. این کار با cacheTag انجام می‌شود. وقتی داخل یک تابع که با 'use cache' کش می‌شود از cacheTag('products') استفاده می‌کنیم، در واقع می‌گوییم این داده‌ها متعلق به تگ «products» هستند. بعداً هر جا لازم باشد می‌توان تمام کش‌هایی که این تگ را دارند با یک دستور invalidate کرد، بدون اینکه لازم باشد مسیر یا تابع خاصی را بشناسیم.

برای نامعتبر کردن این کش‌ها دو روش اصلی وجود دارد: revalidateTag و updateTag. تفاوت این دو در نحوهٔ تازه‌سازی داده است.

revalidateTag از الگوی stale‑while‑revalidate استفاده می‌کند. یعنی وقتی این تابع اجرا شود، کش فعلی فوراً حذف نمی‌شود؛ کاربران همچنان همان دادهٔ کش‌شده (حتی اگر کمی قدیمی باشد) را دریافت می‌کنند تا پاسخ سریع بماند. در همین زمان، Next.js در پس‌زمینه دادهٔ جدید را دوباره از منبع اصلی می‌گیرد و کش را به‌روزرسانی می‌کند. این روش برای داده‌هایی مناسب است که کمی تأخیر در به‌روزرسانی آن‌ها مشکلی ایجاد نمی‌کند، مثل لیست محصولات، مقالات وبلاگ یا داده‌های عمومی سایت.

در مقابل، updateTag کش را بلافاصله منقضی می‌کند. یعنی وقتی اجرا شود، دادهٔ کش‌شده دیگر قابل استفاده نیست و درخواست بعدی مجبور است مستقیماً دادهٔ تازه را از منبع اصلی دریافت کند. هدف این روش سناریوی read‑your‑own‑writes است؛ یعنی وقتی کاربر تغییری ایجاد می‌کند (مثلاً یک پست می‌سازد یا اطلاعاتی را ویرایش می‌کند)، باید همان لحظه نتیجهٔ تغییر خودش را ببیند و نه نسخهٔ قدیمی کش‌شده.

از نظر محل استفاده نیز تفاوتی وجود دارد: revalidateTag را می‌توان هم در Server Actions و هم در Route Handlers استفاده کرد، اما updateTag فقط در Server Actions قابل استفاده است. به‌طور خلاصه، اگر سرعت پاسخ مهم‌تر از به‌روزرسانی فوری باشد از revalidateTag استفاده می‌شود، اما اگر لازم باشد تغییرات بلافاصله برای کاربر قابل مشاهده باشند باید از updateTag استفاده کرد.

در دستور revalidateTag('user', 'max')، آرگومان دوم مشخص می‌کند چه مدت اجازه داریم محتوای stale را نمایش دهیم در حالی که نسخهٔ جدید در پس‌زمینه ساخته می‌شود.

به بیان ساده:

  • وقتی revalidateTag اجرا می‌شود، کش فوراً حذف نمی‌شود.

  • کاربران همچنان نسخهٔ قبلی (stale) را می‌بینند.

  • همزمان Next.js در background دادهٔ جدید را تولید می‌کند.

  • این «بازهٔ مجاز برای نمایش stale» با آرگومان دوم تعیین می‌شود.

اگر این بازه تمام شود و هنوز نسخهٔ جدید آماده نباشد، درخواست‌های بعدی منتظر می‌مانند تا دادهٔ تازه آماده شود (یعنی دیگر stale سرو نمی‌شود).

استفاده از 'max' یعنی:

طولانی‌ترین بازهٔ ممکن برای سرو کردن stale را بده تا احتمال بلاک شدن درخواست‌ها حداقل شود.

✅ کاربرد عملی:

وقتی performance مهم‌تر از نمایش فوری تغییرات است (مثل صفحات عمومی)، معمولاً 'max' انتخاب مناسبی است.

مفهوم revalidatePath

برای زمانی است که می‌خواهی کش مربوط به یک مسیر (route) در Next.js نامعتبر شود، بدون اینکه لازم باشد بدانی داخل آن صفحه چه داده‌هایی با چه tagهایی کش شده‌اند.

منطقش این است که Next.js برای هر صفحه یا route، نتیجهٔ رندر و داده‌های وابسته به آن را در کش نگه می‌دارد. وقتی revalidatePath('/profile') را صدا بزنی، به Next.js می‌گویی:

«هر چیزی که برای رندر صفحهٔ /profile کش شده، دیگر معتبر نیست و باید دوباره ساخته شود.»

در نتیجه درخواست بعدی برای آن مسیر باعث می‌شود صفحه دوباره رندر شود و داده‌ها دوباره از منبع اصلی گرفته شوند.

نکتهٔ مهم این است که revalidatePath بر اساس route کار می‌کند، نه بر اساس داده. یعنی اگر صفحهٔ /profile چندین تابع کش‌شده، چند fetch یا چند component داشته باشد، همهٔ آن‌ها برای آن مسیر نامعتبر می‌شوند. به همین دلیل می‌گویند این روش کم‌دقت‌تر است و ممکن است کش بیشتری از حد لازم را invalid کند.

به همین دلیل در مستندات توصیه شده که اگر می‌توانی از tag-based invalidation استفاده کنی (cacheTag + revalidateTag یا updateTag). چون در آن حالت فقط دادهٔ خاصی که تغییر کرده invalidate می‌شود، نه کل صفحه.

از revalidatePath معمولاً در این موقعیت‌ها استفاده می‌شود:

اول، وقتی صفحه‌ای داری که tagگذاری نکرده‌ای و نمی‌دانی چه داده‌هایی داخلش کش شده‌اند. در این حالت ساده‌ترین راه این است که کل مسیر را revalidate کنی.

دوم، وقتی یک صفحهٔ مشخص باید بعد از یک mutation دوباره ساخته شود. مثلاً کاربر پروفایلش را ویرایش می‌کند و می‌خواهی دفعهٔ بعد که /profile باز شد حتماً نسخهٔ جدید رندر شود.

سوم، برای صفحات استاتیک یا prerender شده که بعد از تغییر داده باید دوباره ساخته شوند.

خلاصهٔ منطقی تفاوت‌ها:

  • revalidatePath → invalid کردن کش در سطح صفحه / route

  • revalidateTag → invalid کردن کش در سطح داده

  • updateTag → invalid فوری داده برای اینکه کاربر فوراً تغییر خودش را ببیند

قاعدهٔ عملی که اکثر پروژه‌ها استفاده می‌کنند این است:

اگر داده‌ها را tag کرده‌ای، از tag-based invalidation استفاده کن.

اگر نمی‌دانی داده‌ها چه tagی دارند یا می‌خواهی کل یک صفحه دوباره ساخته شود، از revalidatePath استفاده کن.

تکنیک جذاب برای مدیریت فایل های CSS برای افزایش performance
https://nextjs.org/docs/app/api-reference/config/next-config-js/cssChunking

رفتار کامپوننت <Image> در Next.js در Import کردن

https://nextjs.org/docs/app/getting-started/images#remote-images

۱. روش Import (ایستاتیک یا داینامیک):

  • نحوه کار: از طریق import pic from '@/public/pic.jpg' یا await import(...).

  • پردازش فایل: Next.js (توسط Webpack/Turbopack) فایل فیزیکی را از روی هارد می‌خواند.

  • مزایا (خودکار):

  • ابعاد (width و height) را خودش تشخیص می‌دهد.

  • نسخه تار تصویر (Base64) را برای لودینگ (placeholder="blur") می‌سازد.

  • فضای تصویر را در صفحه رزرو می‌کند تا از پرش صفحه (CLS) جلوگیری شود.

۲. روش String URL (آدرس متنی):

  • نحوه کار: ارسال مستقیم آدرس به صورت رشته متنی مثل src="/images/pic.jpg".

  • پردازش فایل: فایلی پردازش نمی‌شود؛ این فقط یک آدرس است که به مرورگر کاربر داده می‌شود تا بعداً دانلودش کند.

  • محدودیت‌ها:

  • Next.js ابعاد فایل را نمی‌داند.

  • الزام: توسعه‌دهنده باید حتماً width و height را دستی وارد کند (یا از پروپ fill استفاده کند).

  • امکان استفاده از placeholder="blur" به صورت خودکار وجود ندارد.

💡 علت فنی این تفاوت:

Next.js برای جلوگیری از به هم ریختگی چیدمان سایت (Layout Shift) باید از قبل سایز عکس را بداند. در روش Import چون فایل خوانده می‌شود سیستم سایز را می‌فهمد، اما در روش String چون فقط یک آدرس اینترنتی پاس داده شده، سیستم غیب‌گو نیست و از شما می‌خواهد سایز را صراحتاً اعلام کنید.

دقیقا Edge چیه؟
در حالت سلف‌هاست (Docker)، کلمه Edge به Edge Runtime (محیط اجرای کدهای جاوااسکریپت) اشاره دارد، نه یک مکان فیزیکی.

قضیه از این قرار است:

۱. محیط اجرای متفاوت: در کانتینر داکر شما، Middleware با موتور کامل و سنگین Node.js اجرا نمی‌شود. بلکه در یک محیط بسیار سبک، ایزوله و سریع به نام Edge Runtime (مبتنی بر V8) اجرا می‌شود.

۲. نگهبانِ دمِ در: این محیطِ سبک، دقیقاً در “نقطه ورود” درخواست به کانتینر داکر شما قرار دارد. یعنی قبل از اینکه موتور اصلی Node.js بخواهد روتینگ را انجام دهد یا صفحه‌ای را رندر کند، این نگهبان در کسری از میلی‌ثانیه درخواست را بررسی می‌کند.

۳. محدودیت‌ها: چون این محیط خیلی سبک است، به امکانات سنگین Node.js (مثل fs برای خواندن فایل یا درایورهای مستقیم دیتابیس SQL) دسترسی ندارد. فقط می‌تواند کارهای سریع شبکه (مثل زدن API به Redis) را انجام دهد.

پس در سناریوی شما:

وقتی می‌گوییم “محدودسازی در لبه”، یعنی به محض اینکه درخواست به کانتینر داکر شما رسید، این میدل‌ورِ بسیار سبک (بدون درگیر کردن پردازنده برای بالا آوردن کل چرخه حیات Node.js و Next.js) به Redis شما درخواستی می‌زند و اگر نیاز بود، کاربر را همان جلوی در بلاک می‌کند. این کار باعث می‌شود سرور اصلی شما زیر بار درخواست‌های اسپم نرود.