انتخاب محل اجرای کد و کاهش JavaScript سمت کلاینت

در درس‌های این فصل، همه‌ی ابزارها را جداگانه شناختیم: Server Component، Client Component، مرز "use client"، Props و الگوی ترکیب. حالا باید بتوانیم برای هر بخش از رابط کاربری تصمیم بگیریم که کجا اجرا شود. این تصمیم مستقیماً روی حجم JavaScript، سرعت تعاملی‌شدن صفحه و امنیت اثر می‌گذارد. در این درس یک روش تصمیم‌گیری ساده یاد می‌گیریم، تکنیک‌های کم‌کردن JavaScript را مرور می‌کنیم و سپس Client Componentهای DevNotes را بازبینی می‌کنیم؛ نتیجه این است که دو کامپوننت را کلاً حذف می‌کنیم.

هزینه‌ی واقعی Client Component چیست؟

Client Component هم برای HTML اولیه روی سرور رندر می‌شود، پس کاربر محتوا را زود می‌بیند. اما هزینه‌ی آن بعد از این مرحله شروع می‌شود:

  1. دانلود: کد کامپوننت و تمام کتابخانه‌هایی که import می‌کند باید به مرورگر برسد.
  2. پردازش: مرورگر باید این JavaScript را تجزیه و اجرا کند.
  3. Hydration: React باید رویدادها را به HTML موجود وصل کند. تا این کار تمام نشود، دکمه‌ها کار نمی‌کنند.

روی سیستم‌های ضعیف و شبکه‌های کند، هر کیلوبایت JavaScript اضافه، زمان انتظار کاربر را بیشتر می‌کند. Server Component چنین هزینه‌ای برای مرورگر ندارد: فقط خروجی آن (HTML و داده‌ی فشرده) می‌رسد، نه کدش.

روش تصمیم‌گیری: سؤال‌ها به ترتیب

برای هر کامپوننت این سؤال‌ها را به ترتیب بپرسید و در اولین پاسخ «بله» توقف کنید:

سؤال اگر بله
۱. به داده‌ی پایگاه داده، فایل، راز یا کد سروری نیاز دارد؟ Server Component (و فایل‌های سروری با server-only)
۲. فقط محتوا را نمایش می‌دهد و تعاملی نیست؟ Server Component
۳. با HTML و CSS بومی می‌شود همان رفتار را ساخت؟ (مثل details، فرم GET، Link، :hover) Server Component با HTML بومی
۴. به State، Effect، رویداد، API مرورگر یا Hookهای مسیر نیاز دارد؟ Client Component، ولی تا حد امکان کوچک

ترتیب مهم است: سؤال سوم همان چیزی است که اغلب فراموش می‌شود. مرورگر قابلیت‌های زیادی دارد که بدون JavaScript کار می‌کنند.

تکنیک‌های کم‌کردن JavaScript سمت کلاینت

۱. مرز Client را به برگ‌ها ببرید

اگر فقط یک دکمه به State نیاز دارد، فقط همان دکمه Client باشد، نه کل کارت یا صفحه. در درس «Client Components و دستور use client» دیدیم که "use client" روی همه‌ی ماژول‌های import‌شده اثر می‌گذارد؛ پس هرچه مرز بالاتر باشد، کد بیشتری به مرورگر می‌رود.

text
بد:  Page (Client) → Header، Card، Card، LikeButton، Footer
خوب: Page (Server) → Header، Card، Card، LikeButton (Client)، Footer

۲. محتوای سروری را با children بدهید

اگر Client Component محتوای بزرگی دارد که خودش تعاملی نیست، آن را از بیرون و با children بگیرد (درس «ترکیب Server و Client Components»). محتوا روی سرور رندر می‌شود و کدش به مرورگر نمی‌رود.

۳. کار سنگین را روی سرور انجام بدهید

کتابخانه‌هایی که فقط برای ساختن خروجی استفاده می‌شوند (تبدیل Markdown به HTML، رنگ‌آمیزی کد، قالب‌بندی‌های سنگین، پردازش تاریخ) را در Server Component به کار ببرید تا در بسته‌ی مرورگر قرار نگیرند. مثال مفهومی برای نوشته‌های DevNotes که بعداً ممکن است Markdown باشند:

tsx
// نمونه‌ی مفهومی؛ در DevNotes فعلاً Markdown نداریم
import { renderMarkdown } from "@/lib/markdown"; // کتابخانه‌ی سنگین

export default async function PostBody({ markdown }: { markdown: string }) {
  const html = await renderMarkdown(markdown); // فقط روی سرور اجرا می‌شود
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

اگر PostBody Client Component بود، کل کتابخانه‌ی Markdown به مرورگر می‌رفت. نکته‌ی امنیتی: dangerouslySetInnerHTML فقط برای HTMLی که خودتان ساخته‌اید و پاک‌سازی کرده‌اید مجاز است؛ برای محتوای تولیدشده‌ی کاربران بدون پاک‌سازی (Sanitize) استفاده نکنید.

۴. از قابلیت‌های بومی HTML و CSS استفاده کنید

نیاز راه‌حل بدون JavaScript به‌جای
باز و بسته‌شدن یک بخش <details> و <summary> Client Component با State
جست‌وجو و فیلتر با تغییر آدرس فرم با متد GET Client Component با useRouter
ناوبری بین صفحه‌ها Link router.push در onClick
حالت‌های hover، focus و فعال CSS (:hover، :focus-visible، :has()) State برای هر حالت
انتخاب از بین چند گزینه (تب‌ها، آکاردئون ساده) ورودی‌های radio یا details با CSS State در JavaScript

۵. وابستگی‌های Client را وزن‌کشی کنید

هر import در یک Client Component یعنی کد بیشتر برای مرورگر. قبل از نصب کتابخانه برای یک کار کوچک بپرسید: آیا چند خط JavaScript ساده یا یک قابلیت بومی کافی نیست؟ و آیا کتابخانه واقعاً باید در مرورگر اجرا شود؟ روش‌های جدا‌کردن بخش‌های سنگین با بارگذاری تنبل (Lazy Loading) را در درس «Lazy Loading و بارگذاری بهینه» می‌بینیم.

۶. فقط داده‌ی لازم را بفرستید

JavaScript تنها منبع بار سمت کلاینت نیست؛ Props هم در پاسخ شبکه قرار می‌گیرند (درس «Props، انتقال داده و مرز Server/Client»). آرایه‌ی بزرگی از داده به Client Component دادن، حجم پاسخ را بالا می‌برد.

چگونه حجم JavaScript را اندازه بگیریم؟

حدس‌زدن کافی نیست. در Next.js 16 گزارش next build دیگر ستون First Load JS را نشان نمی‌دهد (درس «مهم‌ترین تغییرات Next.js 16»)، پس از ابزارهای مرورگر استفاده می‌کنیم:

  1. پروژه را با npm run build و npm run start اجرا کنید؛ حالت توسعه حجم واقعی را نشان نمی‌دهد.
  2. ابزار توسعه‌دهنده‌ی مرورگر را باز کنید و به تب Network بروید.
  3. فیلتر JS را انتخاب کنید و گزینه‌ی «Disable cache» را روشن کنید.
  4. صفحه را ریفرش کنید و در نوار پایین، تعداد درخواست‌ها و مجموع حجم «transferred» را یادداشت کنید.
  5. برای نگاه جامع‌تر، گزارش Lighthouse را هم بگیرید (درس «Core Web Vitals و بررسی عملکرد»).

اعداد دقیق به سیستم و نسخه‌ی پروژه بستگی دارد؛ مهم مقایسه‌ی «قبل» و «بعد» روی یک سیستم و یک صفحه است.

عملی: بازبینی Client Componentهای DevNotes

تا اینجا شش Client Component ساخته‌ایم. هرکدام را با سؤال‌های بالا بررسی می‌کنیم:

کامپوننت چرا Client است؟ تصمیم
NavLink usePathname برای تشخیص لینک فعال و useLinkStatus می‌ماند؛ نیاز واقعی دارد و کوچک است
SearchBox State و useRouter می‌ماند؛ بعداً می‌توان با فرم GET ساده جایگزین کرد (درس «searchParams و Query String» نسخه‌ی بدون JavaScript آن را دارد)، ولی برای آموزش الگوی useRouter نگهش داشتیم
CopyLinkButton رویداد کلیک و navigator.clipboard می‌ماند؛ API مرورگر لازم است
ReadingProgress شنونده‌ی اسکرول (Effect) می‌ماند؛ به رویداد مرورگر وابسته است
BackButton useRouter برای router.back() حذف می‌شود؛ یک Link به فهرست مقصد مطمئن‌تری است
Collapsible State باز و بسته بودن حذف می‌شود؛ details و summary بومی همین کار را بدون JavaScript می‌کنند

چرا BackButton حذف می‌شود؟

در درس «مسیریابی برنامه‌ای با useRouter» هشدار دادیم که router.back() وقتی کاربر مستقیم با آدرس وارد صفحه شده، او را از سایت بیرون می‌برد یا کاری نمی‌کند. یک لینک به /posts هم مقصد قطعی دارد، هم Server Component است، هم کلیک راست و باز کردن در تب جدید را پشتیبانی می‌کند.

چرا Collapsible حذف می‌شود؟

Collapsible مثال خوبی برای الگوی ترکیب با children بود، ولی برای کار واقعی ما لازم نیست: عنصر details وضعیت باز و بسته را خودش مدیریت می‌کند، با صفحه‌کلید و صفحه‌خوان سازگار است و حتی قبل از Hydration هم کار می‌کند. الگوی children همچنان برای نیازهایی که واقعاً State دارند (مثل منو با انیمیشن پیچیده یا Provider) مفید است.

تغییر در صفحه‌ی نوشته

فایل متأثر: app/(public)/posts/[slug]/page.tsx. کد کامل فایل پس از تغییر:

tsx
// app/(public)/posts/[slug]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
import CopyLinkButton from "@/components/CopyLinkButton";
import ReadingProgress from "@/components/ReadingProgress";
import RelatedPosts from "@/components/RelatedPosts";
import { formatDate } from "@/lib/format";
import { getAllPosts, getPostBySlug } from "@/lib/posts";

type PostPageProps = {
  params: Promise<{ slug: string }>;
};

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage({ params }: PostPageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) {
    notFound();
  }

  return (
    <main>
      <ReadingProgress />
      <p>
        <Link href="/posts">بازگشت به فهرست نوشته‌ها</Link>
      </p>

      <article>
        <h1>{post.title}</h1>
        <p>
          <time dateTime={post.publishedAt}>
            {formatDate(post.publishedAt)}
          </time>
        </p>
        <p>{post.body}</p>
        <CopyLinkButton path={`/posts/${post.slug}`} />
      </article>

      <details>
        <summary>نوشته‌های دیگر</summary>
        <RelatedPosts currentSlug={post.slug} />
      </details>
    </main>
  );
}

توضیح تغییرها

  • لینک «بازگشت به فهرست نوشته‌ها» جای BackButton را گرفته است. دو import مربوط به BackButton و Collapsible حذف شده‌اند.
  • RelatedPosts همچنان یک Server Component async است و مستقیم داخل details قرار گرفته است. چون همه‌چیز در سرور رندر می‌شود، نیازی به الگوی children و پوسته‌ی Client نیست.
  • صفحه همچنان فقط دو Client Component دارد: ReadingProgress و CopyLinkButton، و هر دو نیاز واقعی به مرورگر دارند.

پاک‌سازی فایل‌ها

دو فایل دیگر استفاده نمی‌شوند و باید حذف شوند:

  • components/BackButton.tsx
  • components/Collapsible.tsx

اگر مطمئن نیستید جای دیگری از آن‌ها استفاده شده، در VS Code با جست‌وجوی سراسری (Ctrl + Shift + F) نام‌هایشان را جست‌وجو کنید. بعد از حذف، npm run build را اجرا کنید تا اگر import مانده‌ای هست، خطا بدهد.

خروجی و رفتار مورد انتظار

  • صفحه‌ی نوشته مثل قبل نمایش داده می‌شود، اما بالای آن یک لینک متنی به فهرست دارد.
  • بخش «نوشته‌های دیگر» با کلیک باز و بسته می‌شود؛ مثلث کنار عنوان وضعیت را نشان می‌دهد.
  • در Production، مجموع JavaScript این صفحه کمی کمتر از قبل است. چون کامپوننت‌هایی که حذف شدند خیلی کوچک بودند، تفاوت محسوسی نخواهد بود؛ بخش عمده‌ی JavaScript صفحه، خود React و Next.js است که در همه‌ی صفحات مشترک است. اهمیت این تمرین در تصمیم‌گیری است، نه در اندازه‌ی این حذف خاص. تأثیر اصلی وقتی دیده می‌شود که کامپوننت یا کتابخانه‌ی سنگین را از مرورگر دور نگه دارید.

نکته‌های مهم

  • بهینه‌سازی بی‌اندازه نکنید. گاهی ساده‌ترین کد یک Client Component کوچک است و تلاش برای حذف آن هزینه‌ی نگهداری بیشتری دارد. اصل این است که بدون دلیل Client نکنید، نه اینکه به هر قیمتی Client را حذف کنید.
  • HTML بومی هم محدودیت دارد. مثلاً details بدون JavaScript نمی‌تواند هنگام باز شدن، یک بخش دیگر را ببندد (آکاردئون انحصاری)، مگر با ویژگی name که پشتیبانی مرورگرها از آن ممکن است متفاوت باشد؛ پشتیبانی را قبل از استفاده بررسی کنید.
  • Server Component هزینه‌ی سرور دارد. جابه‌جاکردن کار به سرور حجم مرورگر را کم می‌کند، اما اجرای آن را روی سرور می‌برد. برای کارهای سنگین، کش و رندر استاتیک اهمیت پیدا می‌کنند (فصل «Cache و Revalidation در Next.js 16»).
  • معیار نهایی کاربر است. قبل و بعد از هر تغییر اندازه‌گیری کنید و فقط وقتی تغییر را نگه دارید که سادگی یا سرعت واقعاً بهتر شده باشد.

جمع‌بندی

  • هزینه‌ی Client Component شامل دانلود، پردازش و Hydration است؛ Server Component این هزینه را برای مرورگر ندارد.
  • ترتیب تصمیم‌گیری: داده یا کد سروری ← محتوای ثابت ← قابلیت بومی HTML و CSS ← در نهایت Client Component کوچک.
  • تکنیک‌های اصلی: مرز Client را به برگ‌ها ببرید، محتوای سروری را با children بدهید، کار سنگین را روی سرور انجام دهید، از details و فرم GET و Link استفاده کنید و Props را حداقلی نگه دارید.
  • اندازه‌گیری با تب Network مرورگر در حالت Production و ابزارهایی مثل Lighthouse انجام می‌شود، چون گزارش Build در Next.js 16 معیار First Load JS ندارد.
  • در DevNotes، BackButton و Collapsible حذف شدند و اکنون چهار Client Component داریم: NavLink، SearchBox، CopyLinkButton و ReadingProgress.
  • در درس بعد، «آزمون فصل چهارم»، دانسته‌های فصل «Server Components و Client Components» را می‌سنجیم.