Prefetching و رفتار Navigation

در درس‌های قبل با Link و useRouter بین صفحات جابه‌جا شدیم و دیدیم ناوبری بدون بارگذاری کامل صفحه انجام می‌شود. حالا می‌خواهیم بفهمیم پشت این سرعت چه اتفاقی می‌افتد. Next.js پیش از اینکه کاربر روی لینکی کلیک کند، بخشی از مسیر مقصد را در پس‌زمینه بارگیری می‌کند (Prefetching) و هنگام کلیک، فقط بخش تغییرکرده‌ی صفحه را عوض می‌کند. در این درس این رفتار را می‌شناسیم، می‌فهمیم چرا مسیرهای پویا گاهی کند به نظر می‌رسند و ابزارهایی برای بهبود تجربه‌ی کاربر یاد می‌گیریم.

ناوبری در Next.js چگونه کار می‌کند؟

وقتی روی لینک معمولی کلیک می‌کنید، مرورگر کل سند HTML را دوباره می‌گیرد. در Next.js چهار ساز‌وکار با هم کار می‌کنند:

  1. رندر روی سرور: مسیرها به‌صورت پیش‌فرض روی سرور رندر می‌شوند.
  2. Prefetching: مسیرهایی که با Link در دید کاربر (Viewport) قرار می‌گیرند، پیشاپیش و در پس‌زمینه بارگیری می‌شوند.
  3. Streaming: بخش‌های آماده‌ی صفحه می‌توانند زودتر برسند و بخش‌های دیرتر بعداً کامل شوند. جزئیات در فصل «Loading، Error و Streaming».
  4. ناوبری سمت کلاینت: به‌جای بارگذاری کامل، فقط بخش‌های تغییرکرده عوض می‌شوند و Layoutهای مشترک (که در درس «Nested Layoutها» دیدیم) حفظ می‌شوند.

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

Prefetching چیست؟

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

مقدار پیش‌واکشی به نوع مسیر مقصد بستگی دارد:

نوع مسیر مقصد رفتار Prefetch پیش‌فرض
مسیر استاتیک (Static) کل مسیر پیشاپیش بارگیری می‌شود.
مسیر پویا (Dynamic) با فایل loading.tsx تا نزدیک‌ترین loading.tsx به‌صورت جزئی بارگیری می‌شود؛ یعنی Layoutهای مشترک و اسکلت انتظار از قبل آماده‌اند.
مسیر پویا بدون loading.tsx Prefetch رد می‌شود یا فقط بخش کمی از آن انجام می‌شود.

دلیل این تفاوت منطقی است: مسیر استاتیک از قبل آماده است و بارگیری پیشاپیش‌اش ارزان است، ولی مسیر پویا باید برای هر درخواست روی سرور ساخته شود و پیش‌واکشی کامل آن کار اضافه‌ی بی‌مورد روی سرور می‌گذارد، چون شاید کاربر هرگز آن لینک را باز نکند.

مسیر «پویا» در اینجا یعنی صفحه‌ای که برای هر درخواست رندر می‌شود، مثلاً چون searchParams یا cookies() می‌خواند. این با «مسیری که قطعه‌ی پویای [slug] دارد» فرق دارد: مسیر /posts/[slug] با generateStaticParams می‌تواند از قبل ساخته شود. تفاوت دقیق رندر پویا و استاتیک را در فصل «Rendering و Data Fetching» می‌بینیم.

دو نکته درباره‌ی نسخه‌ی توسعه

  • Prefetch فقط در Production فعال است. در حالت npm run dev این رفتار را مثل واقعیت نمی‌بینید. برای دیدنش باید npm run build و npm run start را اجرا کنید.
  • Next.js 16 سیستم Prefetch را بازنویسی کرده است: Layoutی که بین چند مسیر مشترک است فقط یک‌بار دانلود می‌شود و فقط بخش‌هایی که هنوز در حافظه نیستند پیش‌واکشی می‌شوند. این‌ها خودکار هستند و کدی برای فعال‌کردنشان لازم نیست. ممکن است تعداد درخواست‌های Prefetch بیشتر ولی حجم هرکدام کمتر دیده شود.

کنترل Prefetch با ویژگی prefetch

کامپوننت Link ویژگی prefetch دارد که سه حالت دارد:

مقدار معنی
بدون مقدار (پیش‌فرض) Next.js بر اساس نوع مسیر تصمیم می‌گیرد (جدول بالا).
prefetch={true} همیشه کل مسیر و داده‌اش پیشاپیش بارگیری می‌شود.
prefetch={false} Prefetch غیرفعال است، هم برای ورود به Viewport و هم هنگام قرارگرفتن ماوس روی لینک.
tsx
<Link href="/posts">نوشته‌ها</Link>
<Link href="/dashboard" prefetch={true}>داشبورد</Link>
<Link href="/about" prefetch={false}>درباره</Link>

در بیشتر موارد رفتار پیش‌فرض بهترین انتخاب است. prefetch={false} برای وقتی مفید است که صفحه لینک‌های خیلی زیادی دارد (مثل جدولی با صدها ردیف) و نمی‌خواهید همه‌ی آن‌ها درخواست پس‌زمینه ایجاد کنند. prefetch={true} هم فقط برای لینک‌های بسیار مهمی که احتمال کلیکشان زیاد است منطقی است، چون بار بیشتری روی سرور می‌گذارد.

ابزار دستی هم داریم: router.prefetch(href) که در درس «مسیریابی برنامه‌ای با useRouter» دیدیم. این روش فقط برای موارد خاص (مثلاً پیش‌واکشی بعد از یک رویداد) کاربرد دارد.

نقش loading.tsx در سرعت ناوبری

فایل loading.tsx که در درس «فایل‌های ویژه‌ی App Router» معرفی کردیم، برای مسیرهای پویا یک نقش مضاعف دارد:

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

بدون این فایل، کلیک روی لینک یک مسیر پویا ممکن است تا رسیدن پاسخ سرور هیچ تغییری روی صفحه ایجاد نکند و کاربر فکر کند برنامه قفل کرده است. توصیه‌ی Next.js این است که برای مسیرهای پویا loading.tsx اضافه شود. نحوه‌ی ساخت و طراحی آن را در درس «فایل loading.tsx» و «طراحی وضعیت‌های انتظار و خطا در DevNotes» انجام می‌دهیم؛ فعلاً فقط بدانید چنین ابزاری برای این مشکل وجود دارد.

شبکه‌ی کند و useLinkStatus

حتی با Prefetch، در شبکه‌ی کند یا ناپایدار ممکن است پیش‌واکشی تا لحظه‌ی کلیک تمام نشود. در این حالت کاربر کلیک می‌کند و تا آماده‌شدن مسیر هیچ بازخوردی نمی‌بیند. Hook با نام useLinkStatus وضعیت «در حال ناوبری» (pending) یک لینک را می‌دهد تا بتوانیم یک نشانه‌ی بصری نشان دهیم.

  • این Hook را از next/link وارد می‌کنیم.
  • باید داخل کامپوننتی استفاده شود که درون خود Link رندر می‌شود؛ چون وضعیت همان لینکِ والد را گزارش می‌دهد.
  • فقط در Client Component کار می‌کند و در Pages Router همیشه { pending: false } برمی‌گرداند.
  • مقدار pending برابر true می‌شود وقتی پیش‌واکشی غیرفعال یا هنوز در جریان است و ناوبری منتظر آن مانده است، یا وقتی مسیر مقصد پویا است و loading.tsx ندارد.
  • مستندات رسمی آن را یک «وصله‌ی سریع» می‌داند: اگر ناوبری کند است، ریشه‌ی مشکل را با Prefetch یا loading.tsx حل کنید.

عملی: نشانه‌ی انتظار در منوی DevNotes

فایل متأثر: components/NavLink.tsx که در درس «لینک‌سازی با Link» ساختیم. یک کامپوننت کوچک PendingHint اضافه می‌کنیم و آن را داخل Link قرار می‌دهیم:

tsx
// components/NavLink.tsx
"use client";

import type { ReactNode } from "react";
import Link, { useLinkStatus } from "next/link";
import { usePathname } from "next/navigation";

type NavLinkProps = {
  href: string;
  children: ReactNode;
};

// باید داخل Link رندر شود تا وضعیت همان لینک را بخواند.
function PendingHint() {
  const { pending } = useLinkStatus();

  return <span aria-hidden="true">{pending ? " …" : null}</span>;
}

export default function NavLink({ href, children }: NavLinkProps) {
  const pathname = usePathname();

  const isActive =
    href === "/"
      ? pathname === "/"
      : pathname === href || pathname.startsWith(`${href}/`);

  return (
    <Link href={href} aria-current={isActive ? "page" : undefined}>
      {children}
      <PendingHint />
    </Link>
  );
}

توضیح کد

  • PendingHint یک کامپوننت جدا است چون useLinkStatus باید در کامپوننتِ فرزند لینک فراخوانی شود، نه در همان کامپوننتی که خود Link را می‌سازد.
  • وقتی pending درست باشد، یک «…» کنار متن لینک ظاهر می‌شود.
  • aria-hidden="true" باعث می‌شود صفحه‌خوان‌ها این نشانه‌ی تزئینی را نخوانند.
  • بقیه‌ی کد همان است که از قبل داشتیم و لینک فعال را با aria-current علامت می‌زند.

در شبکه‌ی سریع و روی سیستم خودتان این نشانه تقریباً هیچ‌وقت دیده نمی‌شود. برای دیدنش در ابزار توسعه‌ی مرورگر، تب Network، گزینه‌ی کندکردن شبکه (مثلاً Slow 4G یا Fast 3G) را فعال کنید و روی لینکی کلیک کنید که هنوز پیش‌واکشی نشده است.

عملی: دیدن Prefetch در Production

برای مشاهده‌ی واقعی Prefetch این مراحل را انجام دهید:

  1. سرور توسعه را با Ctrl + C متوقف کنید.
  2. دستور npm run build را اجرا کنید و منتظر پایان بماند.
  3. دستور npm run start را بزنید و http://localhost:3000 را باز کنید.
  4. ابزار توسعه‌ی مرورگر را باز کنید، به تب Network بروید و فیلتر Fetch/XHR را انتخاب کنید.
  5. صفحه را ریفرش کنید. بدون هیچ کلیکی، درخواست‌هایی برای مسیرهایی که در هدر لینک شده‌اند ظاهر می‌شود؛ این‌ها درخواست‌های Prefetch هستند و معمولاً پارامتر _rsc در آدرسشان دارند.
  6. حالا روی «نوشته‌ها» کلیک کنید. جابه‌جایی سریع انجام می‌شود و صفحه به‌طور کامل از اول بارگذاری نمی‌شود.

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

سایر رفتارهای ناوبری که خوب است بدانید

  • حفظ Layoutها: هنگام جابه‌جایی بین صفحاتی که Layout مشترک دارند، آن Layout دوباره ساخته نمی‌شود و State کامپوننت‌های کلاینتی داخلش حفظ می‌شود. اگر نیاز دارید State با هر ناوبری ریست شود، از template.tsx استفاده می‌شود که در درس «فایل‌های ویژه‌ی App Router» معرفی کردیم.
  • دکمه‌های بازگشت و جلو: مرورگر هنگام برگشت به صفحات قبلی، معمولاً وضعیت قبلی را برمی‌گرداند و Next.js آن را با تاریخچه هماهنگ نگه می‌دارد. بخشی از نتیجه‌ی ناوبری در حافظه‌ی مرورگر (Client-side Router Cache) نگه داشته می‌شود. طول عمر آن و ارتباطش با بقیه‌ی انواع کش را در درس «تفاوت کش داده، خروجی رندر، Router Cache، Request Memoization و کش مرورگر» بررسی می‌کنیم.
  • محل اسکرول: به‌طور پیش‌فرض بعد از ناوبری به بالای صفحه‌ی جدید می‌رویم. با scroll={false} در Link یا گزینه‌ی { scroll: false } در router.push این رفتار خاموش می‌شود. بعد از ناوبری با دکمه‌ی بازگشت، موقعیت اسکرول قبلی بازیابی می‌شود. در Next.js 16، اگر در CSS برای html مقدار scroll-behavior: smooth گذاشته‌اید و می‌خواهید در ناوبری هم اعمال شود، باید ویژگی data-scroll-behavior="smooth" را روی تگ html بگذارید.
  • History API بومی: می‌توان از window.history.pushState و replaceState مستقیماً استفاده کرد؛ این فراخوانی‌ها با usePathname و useSearchParams هماهنگ می‌شوند. برای نیازهای معمولی ما، Link و useRouter کافی هستند.

وضعیت مسیرهای DevNotes

با در نظر گرفتن حالت پیش‌فرض (بدون cacheComponents) و ساختاری که تا اینجا ساخته‌ایم:

مسیر وضعیت فعلی اثر بر Prefetch
/ و /about استاتیک کل مسیر پیشاپیش بارگیری می‌شود
/posts/[slug] با generateStaticParams برای slugهای نمونه از قبل ساخته می‌شود برای این slugها مثل مسیر استاتیک رفتار می‌کند
/posts پویا، چون searchParams می‌خواند فقط با loading.tsx به‌صورت جزئی پیش‌واکشی می‌شود
/dashboard و زیرمسیرها فعلاً استاتیک (داده‌ی کاربر نداریم) بعد از افزودن احراز هویت پویا می‌شوند

این جدول نشان می‌دهد چرا در فصل «Loading، Error و Streaming» برای /posts و داشبورد حتماً loading.tsx می‌سازیم. هنوز آن فایل را نساخته‌ایم؛ فعلاً کافی است ارتباط را بدانید.

نکته‌های مهم

  • برای مقایسه‌ی سرعت از Production استفاده کنید: آنچه در npm run dev می‌بینید، از نظر Prefetch و سرعت ناوبری، نماینده‌ی تجربه‌ی واقعی کاربر نیست.
  • لینک داخلی را با Link بنویسید: تگ <a> معمولی نه Prefetch دارد و نه ناوبری سمت کلاینت.
  • Prefetch را بی‌دلیل خاموش یا اجباری نکنید: رفتار پیش‌فرض برای بیشتر لینک‌ها مناسب است.
  • useLinkStatus علاج اصلی نیست: اگر ناوبری کند است، اول ببینید مسیر پویا loading.tsx دارد یا نه و آیا می‌تواند استاتیک شود. نشانه‌ی انتظار فقط کمک تجربه‌ی کاربری است.
  • Prefetch مقصد را امن نمی‌کند: پیش‌واکشی یک مسیر خصوصی به معنی دسترسی آزاد نیست؛ هر درخواست باید در سرور مجوز بگیرد. این موضوع را در فصل «Authentication و Security» می‌بینیم.

جمع‌بندی

  • Next.js لینک‌هایی که با Link در Viewport قرار می‌گیرند را در Production پیشاپیش بارگیری می‌کند و ناوبری را سریع‌تر می‌کند.
  • مسیرهای استاتیک کامل پیش‌واکشی می‌شوند؛ مسیرهای پویا یا رد می‌شوند یا فقط تا نزدیک‌ترین loading.tsx به‌صورت جزئی پیش‌واکشی می‌شوند.
  • ویژگی prefetch (مقدارهای پیش‌فرض، true و false) و متد router.prefetch این رفتار را کنترل می‌کنند.
  • اضافه‌کردن loading.tsx برای مسیرهای پویا ناوبری را فوری حس‌پذیر و Prefetch جزئی را ممکن می‌کند.
  • useLinkStatus در شبکه‌ی کند، وضعیت «در حال ناوبری» یک لینک را برای نمایش بازخورد می‌دهد.
  • در DevNotes، NavLink را با یک نشانه‌ی انتظار به‌روز کردیم.
  • در درس بعد، «ساخت فهرست و صفحه‌ی جزئیات نوشته‌ها»، آنچه در این فصل یاد گرفتیم را برای ساخت فهرست و صفحه‌ی جزئیات نوشته‌ها به کار می‌بریم.