بازگشت به وبلاگ

اشتباهات رایج Next.js که بی‌سروصدا پرفورمنس رو می‌کشن

۲ مرداد ۱۴۰۵3 دقیقه مطالعه
اشتباهات رایج Next.js که بی‌سروصدا پرفورمنس رو می‌کشن

بیشتر اپ‌های کندِ Next.js به‌خاطر یه مشکل بزرگ کند نیستن — به‌خاطر چندتا عادت کوچیک و تکرارشونده کندن که بی‌سروصدا هرچی فریم‌ورک مجانی بهت می‌ده رو خنثی می‌کنن. این‌جا اونایی که مدام تکرار می‌شن رو می‌بینیم.

۱. "use client" خیلی بالای درخت

علامت‌گذاری کل یه صفحه یا layout به‌عنوان Client Component فقط چون یه دکمه به onClick نیاز داره، هرچی زیرشه رو توی باندل کلاینت می‌کشونه و رندر سرور رو برای کل اون زیردرخت خاموش می‌کنه. مرز رو تا جای ممکن کوچیک نگه دار — یه صفحه می‌تونه Server Component بمونه با یه جزیره‌ی تعاملی کوچیک توش.

// قبل — کل صفحه از Server Component بودن خارج می‌شه.
"use client";
export default function ProjectPage({ project }) {
  return (
    <article>
      <h1>{project.title}</h1>
      <LikeButton projectId={project.id} />
    </article>
  );
}

// بعد — فقط تیکه‌ی تعاملی، Client Component‌ه.
export default function ProjectPage({ project }) {
  return (
    <article>
      <h1>{project.title}</h1>
      <LikeButton projectId={project.id} />
    </article>
  );
}
// LikeButton.tsx
("use client");
export function LikeButton({ projectId }) {
  /* ... */
}

۲. awaitهای پشت‌سرهمی که نیازی به پشت‌سرهم بودن ندارن

دوتا fetch مستقل که یکی‌یکی await می‌شن، تاخیرشون رو بدون هیچ دلیلی جمع می‌کنن. اگه دومی به نتیجه‌ی اولی وابسته نیست، هم‌زمان شلیکشون کن.

// هر دو درخواست مستقلن — ولی این یکی latencyA + latencyB طول می‌کشه.
const author = await getAuthor(post.authorId);
const comments = await getComments(post.id);

// این یکی max(latencyA, latencyB) طول می‌کشه.
const [author, comments] = await Promise.all([
  getAuthor(post.authorId),
  getComments(post.id),
]);

۳. یه کوئری کند که کل صفحه رو بلاک می‌کنه

بدون یه Suspense boundary دورش، یه منبع داده‌ی کند (یه API خارجی، یه کوئری aggregation سنگین) اولین بایت کل صفحه رو عقب می‌ندازه، درحالی‌که بقیه‌ی محتوا فوری آماده بود.

۴. عکس‌های بدون ابعاد یا priority

یه <img> — یا حتی next/image بدون width/height/fill — تا موقع لود شدنش هیچ جایی رزرو نمی‌کنه، که مستقیم به CLS ضربه می‌زنه. و عکس هیرو بالای fold باید priority بگیره تا پشت بقیه‌چیزها لیزی‌لود نشه.

۵. importهای barrel-file که tree-shaking رو خنثی می‌کنن

import کردن یه آیکون تنها از barrel index غول‌پیکر یه کتابخونه، بسته به اینکه bundler چطور resolve‌ش می‌کنه، می‌تونه خیلی بیشتر از قصدت رو بکشه تو. جایی که کتابخونه پشتیبانی می‌کنه import مستقیم/نام‌دار رو ترجیح بده، و برای اونایی که خودشون خوب tree-shake نمی‌شن optimizePackageImports رو تنظیم کن.

// next.config.ts
const nextConfig = {
  experimental: {
    optimizePackageImports: ["lucide-react", "date-fns"],
  },
};

۶. فراموش‌کردن تنظیمات cache/revalidate

استفاده از هر API داینامیک (cookies()، headers()، یه fetch بدون کش) هرجای یه route، کل اون route رو از رندر استاتیک خارج می‌کنه — اغلب تصادفی، و یه صفحه‌ای که باید آنی باشه رو تبدیل می‌کنه به چیزی که سر هر درخواست از صفر ساخته می‌شه.

۷. گرفتن مجدد همون داده توی کامپوننت‌های خواهر‌وبرادر

چندتا Server Component توی یه درخت درخواست که مستقلاً همون تابع data-access رو صدا می‌زنن، بدون memoization، هرکدوم جدا می‌رن سراغ دیتابیس. پیچوندن تابع توی cache() از React، همه‌شون رو به یه فراخوانی به‌ازای هر درخواست جمع می‌کنه.

هیچ‌کدوم از این‌ها عجیب‌وغریب نیستن — همه‌شون توی code review، یه کامپوننت در یه لحظه، کاملاً منطقی به‌نظر می‌رسن، و فقط وقتی به کل صفحه نگاه کنی جمع می‌شن به یه مشکل واقعی.