در حال دریافت اطلاعات ...
تکنیک بهینه سازی با 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 را در لحظه آپدیت میکنند.
سناریوهای کاربردی:
مدیریت تبها (Tabs):
کاربر در داشبورد خود بین تبهای «پروفایل» و «تنظیمات» جابهجا میشود. آدرس مرورگر تغییر میکند تا اگر لینک را برای کسی فرستاد، دقیقاً همان تب باز شود، اما جابهجایی بین تبها کاملاً سمت کلاینت و بدون بارگذاری مجدد انجام میشود.
کنترل مودالها و پاپآپها:
مودال ورود به سایت باز میشود و آدرس تغییر میکند. بزرگترین مزیت این کار این است که اگر کاربر در گوشی خود دکمه «بازگشت» (Back) را بزند، به جای اینکه کلاً از صفحه قبل خارج شود، فقط مودال بسته میشود.
جستجو و فیلتر زنده (Live Search/Filter):
لیست محصولات از قبل دریافت شده است. کاربر در نوار جستجو تایپ میکند و لیست در لحظه فیلتر میشود. همزمان آدرس مرورگر آپدیت میشود تا وضعیت جستجو در URL ذخیره بماند، بدون اینکه برای هر حرف تایپ شده، سرور درگیر شود.
تغییر حالت نمایش (View Mode):
کاربر نحوه نمایش لیست مقالات را از حالت «جدول» به حالت «کارت» تغییر میدهد. این وضعیت در آدرس ذخیره میشود تا ترجیح کاربر حفظ شود.
صفحهبندی سمت کلاینت (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 (انتقال سمت کلاینت).
برای درک مسیریابی باید ۴ مفهوم را بدانید:
الف) رندر سمت سرور (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) دستنخورده باقی میمانند.
فقط محتوای صفحه جدید جایگزین میشود و اسکرول به بالای صفحه برمیگردد.
گاهی با وجود این بهینهسازیها، سایت کند به نظر میرسد. دلایل آن شامل موارد زیر است:
الف) مسیرهای پویا بدون 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) و انتقال منطقهای پردازشی به سرور.
Next.js به شما اجازه میدهد URL صفحه را بدون رفرش شدن عوض کنید و پارامترها (مثل ?sort=asc) را آپدیت کنید. این کار با توابع بومی جاوا اسکریپت انجام میشود:
الف) window.history.pushState
برای اضافه کردن یک مرحله به تاریخچه (History) مرورگر.
سناریوی واقعی: کاربر در یک فروشگاه، محصولات را روی “ارزانترین” مرتب میکند. URL به ?sort=asc تغییر میکند. اگر دکمه Back (بازگشت) مرورگر را بزند، به حالت بدون فیلتر قبلی برمیگردد.
ب) window.history.replaceState
برای جایگزین کردن URL فعلی بدون اضافه شدن به تاریخچه مرورگر (کاربر نمیتواند دکمه Back را بزند).
سناریوی واقعی: تغییر زبان سایت از انگلیسی به فرانسوی (/en/ به /fr/). در این حالت نمیخواهیم کاربر با زدن دکمه Back مرورگر دوباره به زبان انگلیسی برگردد، چون زبان اصلی خودش را انتخاب کرده است. پس وضعیت فعلی را “جایگزین” میکنیم.
برای درک اینکه چرا ریاکت در لود اولیه مرورگر، کدهای 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) در کار نیست!
دستور 'use client' یک مرز است: وقتی این دستور را مینویسید، به Next.js میگویید از این نقطه به بعد، مسئولیت اجرا با مرورگر است.
قانون آبشاری: هر فایلی که 'use client' دارد و تمام فایلها و ماژولهایی که درون آن import میشوند، کلاینتی محسوب میشوند.
قانون جعبه سیاه: سرور کامپوننتهای کلاینتی را پردازش نمیکند. از نظر سرور، آنها یک “جعبه سیاه” هستند و سرور فقط یک جایخالی (Placeholder) برای آنها در RSC Payload میگذارد.
کامپوننتهای سروری (بدون 'use client'): کد جاوااسکریپت آنها هرگز به مرورگر نمیرود. مرورگر فقط HTML و دیتای ساختاریافته (RSC Payload) آنها را دریافت میکند.
کامپوننتهای کلاینتی (دارای 'use client'): تمام کدهای جاوااسکریپت آنها برای مرورگر ارسال میشود تا بتوانند تعاملی (Hydrate) شوند.
کامپوننتهای مشترک (مثل یک دکمه یا اسپینر بدون 'use client'):
اگر در یک فایل سروری ایمپورت شوند -> سروری میمانند (JS ارسال نمیشود).
اگر در یک فایل کلاینتی ایمپورت شوند -> کلاینتی میشوند (JS آنها به باندل مرورگر اضافه میشود).
برای جلوگیری از کلاینتی شدن کامپوننتهای سروری و افزایش حجم باندل مرورگر، از این الگو استفاده میشود:
❌ روش اشتباه (باعث سنگین شدن باندل کلاینت میشود):
ایمپورت کردن مستقیم یک کامپوننت سروری (مثل Spinner) داخل فایلِ یک کامپوننت کلاینتی (مثل Button).
✅ روش درست (حفظ صفر بایت جاوااسکریپت برای کامپوننت سروری):
کامپوننت کلاینت (Button) را طوری بنویسید که یک prop (مثل children) دریافت کند. سپس در یک فایل سروری (Page)، هر دو را ایمپورت کرده و کامپوننت سروری را درون کامپوننت کلاینتی قرار دهید: <Button> <Spinner /> </Button>.
“ایمپورتِ” فایل در کلاینت = تبدیل شدن به کلاینت و ارسال JS.
“پاس دادن به عنوان Children” از سرور به کلاینت = باقی ماندن در سرور و ارسال نشدن JS.