React
۷ دقیقه مطالعهبهینهسازی عملکرد Next.js با اندازهگیری، نه حدس
با Core Web Vitals، تحلیل bundle، اندازه تصاویر و بررسی مرزهای کلاینت گلوگاهها را پیدا کن و اثر تغییرات را بسنج.

بهینهسازی عملکرد باید بر اساس شواهد باشد. ابتدا خط مبنای قابلتکرار بساز، سپس مشخص کن گلوگاه انتقال شبکه، پاسخ سرور، بارگذاری تصویر و فونت، اجرای JavaScript یا پایداری چیدمان است. این مقاله تکنیکهای عملی Next.js را همراه با توضیح زمان مفیدبودن و محدودیت هرکدام بررسی میکند.
۱. قبل از بهینهسازی معیارها را بشناس
LCP زمان ظاهرشدن بزرگترین عنصر محتوای قابلمشاهده را میسنجد؛ INP پاسخگویی تعاملات واقعی و CLS جابهجایی غیرمنتظره چیدمان را بررسی میکند. TTFB برای تشخیص انتظار تا اولین بایت پاسخ مفید است، درحالیکه اندازه bundle بهتنهایی مقدار JavaScript اجراشده در مرورگر را نشان نمیدهد. امتیاز بالای Lighthouse مفید است، اما داده کاربران واقعی و مسیر استفاده آنها هم اهمیت دارد.
۲. خط مبنای قابل اعتماد بساز
نسخه production را با مسیر، viewport، پروفایل دستگاه، محدودسازی شبکه و نسخه مرورگر یکسان اندازه بگیر. چند بار آزمایش کن و میانه نتایج را مقایسه کن، چون اندازهگیری آزمایشگاهی نوسان دارد. در DevTools از Network برای یافتن منابع حجیم یا دیررس، Performance برای بررسی long task و اجرای اسکریپت و Lighthouse برای ممیزی تکرارپذیر استفاده کن. فرضیه و نتیجه را کوتاه ثبت کن.
# Compare like-for-like runs
1. Run a production build, not only `next dev`.
2. Record route, viewport, device profile, network, and build hash.
3. Run Lighthouse 3-5 times and compare the median, not the best score.
4. Inspect LCP element, long tasks, layout shifts, and network waterfall.
5. Change one bottleneck, repeat the same test, and document the result.۳. استفاده از فونت WOFF2 محلی با next/font/local
WOFF2 معمولاً قالب مناسبی برای فونت وب است، چون داده فونت را فشرده منتقل میکند. زیرمجموعه محلی شامل حروف و وزنهایی را نگه دار که واقعاً استفاده میکنی؛ اگر صفحه فقط دو وزن لازم دارد، همه وزنها و یک خانواده چندزبانه کامل را بارگذاری نکن. next/font/local فونت را self-host میکند و CSS مربوط را در build میسازد و نیاز به درخواست جداگانه به سرویس فونت ثالث را کاهش میدهد. display: "swap" متن را تا زمان بارگذاری فونت قابلمشاهده نگه میدارد؛ preload را فقط برای فونتهای ضروری ابتدای صفحه فعال کن. نام فایل و وزن واقعی پروژه را جایگزین کن.
import localFont from "next/font/local";
const siteFont = localFont({
src: [
{ path: "./fonts/Inter-Regular.woff2", weight: "400", style: "normal" },
{ path: "./fonts/Inter-Medium.woff2", weight: "500", style: "normal" },
{ path: "./fonts/Inter-Bold.woff2", weight: "700", style: "normal" },
],
display: "swap",
variable: "--font-site",
preload: true,
});
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <html lang="en" className={siteFont.variable}><body>{children}</body></html>;
}۴. اتصال فونت به توکنهای طراحی
متغیر CSS را یکبار در ریشه تعریف کن و در سیستم طراحی استفاده کن. اگر Tailwind CSS v4 داری، متغیر را مانند مثال با @theme inline متصل کن. وقتی زبان فارسی یا عربی فعال است بررسی کن فایل فونت واقعاً حروف آن زبان را داشته باشد؛ نبود glyph باعث fallback و تغییر اندازه متن میشود.
@theme inline {
--font-sans: var(--font-site);
}
body {
font-family: var(--font-site), system-ui, sans-serif;
}۵. پیکربندی و اجرای Bundle Analyzer
@next/bundle-analyzer را بهعنوان وابستگی توسعه نصب کن و config نکست را با آن wrap کن. فلگ محیطی باعث میشود تحلیل اختیاری باشد و buildهای معمولی گزارش analyzer تولید نکنند. در ویندوز، cross-env اسکریپت را قابلحمل میکند؛ یا متغیر محیطی را با دستور مخصوص shell خودت تنظیم کن. Analyzer را روی build تولید اجرا کن، نه dev server.
import type { NextConfig } from "next";
import bundleAnalyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
const nextConfig: NextConfig = {};
export default withBundleAnalyzer(nextConfig);{
"scripts": {
"dev": "next dev",
"build": "next build",
"analyze": "cross-env ANALYZE=true next build"
}
}
# Install:
npm install -D @next/bundle-analyzer cross-env
# Run on Windows, macOS, or Linux:
npm run analyze۶. چطور خروجی Analyzer را بخوانیم
گزارش معمولاً treemap است: هر مستطیل یک ماژول را نشان میدهد و مساحت آن سهم تقریبی ماژول از bundle تحلیلشده است. دنبال کتابخانههای غیرمنتظره حجیم، نسخههای تکراری، دادههای locale، مجموعه آیکنها و وابستگیهایی بگرد که Client Component وارد کرده است. گزارش client برای JavaScript مرورگر اهمیت ویژه دارد. زنجیره import را دنبال کن تا بفهمی کدام کامپوننت وابستگی را وارد کرده؛ فقط بهخاطر بزرگبودن مستطیل کتابخانه را حذف نکن، ابتدا نیاز واقعی و امکان import محدودتر را بررسی کن.
۷. جداسازی قابلیتهای سنگین با dynamic import
ممکن است ویرایشگر نمودار، rich text، نقشه یا PDF viewer تا زمان بازکردن پنل لازم نباشد. next/dynamic میتواند کد آن را در chunk جدا قرار دهد. این کار فقط وقتی دانلود اولیه را کم میکند که قابلیت فوراً لازم نباشد. وضعیت بارگذاری مناسبی نمایش بده و waterfall را بررسی کن؛ chunkهای بسیار ریز یا نموداری که در بالای صفحه دیر ظاهر میشود میتواند تجربه را بدتر کند.
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./heavy-chart"), {
loading: () => <div aria-busy="true">Loading chart…</div>,
});
export function AnalyticsPanel() {
return <section><h2>Analytics</h2><HeavyChart /></section>;
}۸. useMemo، useCallback و memo؛ قبل از استفاده اندازه بگیر
useMemo مقدار محاسبهشده را بین رندرها نگه میدارد تا وقتی dependencyها تغییر نکردهاند؛ useCallback مرجع تابع را پایدار میکند؛ و memo میتواند رندر فرزند را در صورت ثابتبودن props رد کند. در مثال، فیلترکردن فهرست بزرگ ممکن است ارزش memoization داشته باشد و callback پایدار به ردیف memoشده کمک میکند. برای فهرست کوچک، پیچیدگی اضافه شاید از سودش بیشتر باشد. این hookها حجم bundle دانلودی را کم نمیکنند و جای profiling را نمیگیرند.
"use client";
import { memo, useCallback, useMemo, useState } from "react";
type Product = { id: string; name: string; price: number };
const ProductRow = memo(function ProductRow({ product, onSelect }: {
product: Product; onSelect: (id: string) => void;
}) {
return <button onClick={() => onSelect(product.id)}>{product.name} — {product.price}</button>;
});
export function ProductList({ products }: { products: Product[] }) {
const [query, setQuery] = useState("");
const filtered = useMemo(() => {
const normalized = query.trim().toLowerCase();
return products.filter((p) => p.name.toLowerCase().includes(normalized));
}, [products, query]);
const handleSelect = useCallback((id: string) => {
console.log("selected", id);
}, []);
return <>
<input value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Search products" />
{filtered.map((product) => <ProductRow key={product.id} product={product} onSelect={handleSelect} />)}
</>;
}۹. تصاویر، LCP و پایداری چیدمان
از next/image همراه ابعاد مشخص یا نسبت تصویر ثابت استفاده کن تا مرورگر فضا را از قبل رزرو کند. مقدار sizes را مطابق عرض واقعی رندر تنظیم کن؛ در غیر این صورت کاربر ممکن است تصویر بیش از حد بزرگ دانلود کند. فقط تصویر احتمالی LCP را اولویت بده، نه همه تصاویر را. در صورت مفیدبودن از فرمتهای جدید استفاده کن و قبل از تغییر فرمت با waterfall تأیید کن که تصویر واقعاً گلوگاه است.
import Image from "next/image";
export function Hero() {
return <Image
src="/images/hero.webp"
alt="Product dashboard preview"
width={1600}
height={900}
priority
sizes="(max-width: 768px) 100vw, 50vw"
/>;
}۱۰. ترتیب عملی بهینهسازی
- مرزهای غیرضروری Client Component و وابستگیهای حجیم کلاینت را حذف کن.
- تصویر LCP را بهینه کن و فقط وزنهای لازم فونت را self-host کن.
- برای محتوای عمومی، در صورت امکان تازگی، از رندر استاتیک یا revalidation استفاده کن.
- ابزارک سنگین و اختیاری را جدا کن؛ محتوای حیاتی بالای صفحه را بیدلیل به تعویق نینداز.
- useMemo/useCallback را فقط برای گلوگاه محاسبه یا رندر اندازهگیریشده بهکار ببر.
- همان تست production را تکرار کن و نتیجه قبل و بعد را مستند کن.