توسعه فرانت‌اند

Performance Optimization در React

تشخیص مسیر کند، State و Props پایدار، memo و Hookهای Cache، React Compiler و کاهش حجم کار؛ مثال کامل React و Vite با Profiler.

1404/11/28 4 دقیقه مطالعه 953 بازدید

کندی یک برنامه React را با یک تعامل مشخص توصیف کنید: تایپ در جست‌وجو، باز کردن پنل، تغییر فیلتر یا ورود به صفحه. «برنامه کند است» برای انتخاب اصلاح کافی نیست. ممکن است زمان در دریافت داده، اجرای محاسبه، Render، Layout یا دانلود تصویر مصرف شود. اضافه کردن memo به همه Componentها فقط یکی از این بخش‌ها را هدف می‌گیرد و شاید هیچ اثری روی مسیر واقعی نداشته باشد. ابتدا کار کاربر و نقطه انتظار را مشخص کنید.

برای مثال فرضی، تغییر یک شمارنده باعث Render دوباره فهرست نتایج می‌شود، در حالی که داده فهرست عوض نشده است. این مسئله با صفحه‌ای که منتظر API است فرق دارد. در اولی، مرز State و پایداری Props قابل بررسی‌اند؛ در دومی، Query، Cache و شبکه اهمیت دارند. اصلاح باید با شواهد همان مسیر توضیح داده شود. درصد افزایش سرعت بدون تعریف دستگاه، نسخه Build و سناریو، گزارش قابل اتکایی از عملکرد محصول نیست.

Render، Commit و نمایش مرورگر را جدا کنید

Render در React برای محاسبه خروجی Component است و هر بار فراخوانی آن به معنی تغییر همه DOM نیست. Commit اعمال تغییرات مربوط را دنبال می‌کند و مرورگر برای Layout و Paint کار دیگری انجام می‌دهد. ممکن است Component ساده زیاد Render شود اما هزینه کمی داشته باشد، در حالی که یک تغییر کوچک Layout گسترده ایجاد کند. تعداد Render به تنهایی هدف بهینه‌سازی نیست؛ زمان و اثر آن روی پاسخ‌گویی کاربر باید معلوم شود.

Profiler رسمی React داده‌هایی مانند actualDuration و baseDuration را برای درخت مورد مشاهده ارائه می‌کند. این عددها درباره هزینه React هستند و همه زمان شبکه یا Paint را توضیح نمی‌دهند. برای تحلیل صفحه، اطلاعات مرورگر هم لازم است. محیط توسعه با محصول یکسان نیست؛ ابزارهای خطایابی و بعضی رفتارهای توسعه هزینه متفاوتی دارند. نتیجه یک بار مشاهده در لپ‌تاپ سریع را به همه دستگاه‌ها تعمیم ندهید.

مدل State را پیش از Cache اصلاح کنید

State موقت را نزدیک بخشی نگه دارید که به آن نیاز دارد. باز بودن یک Tooltip معمولاً نباید همه داشبورد را درگیر کند. انتقال State به بالا وقتی چند مصرف‌کننده به رابطه مشترک نیاز دارند معنی دارد، اما بالا بردن همه چیز برای راحتی دسترسی هزینه و وابستگی ایجاد می‌کند. Context با مقدار گسترده و متغیر نیز ممکن است مصرف‌کنندگان زیادی را به‌روزرسانی کند. مرز داده را با مسئولیت رابط هماهنگ کنید تا تغییر کوچک دامنه مشخصی داشته باشد.

مقدار مشتق‌شده را بدون دلیل به State جدا تبدیل نکنید. اگر فهرست از Query و داده به دست می‌آید، ذخیره نسخه دوم و هماهنگ کردن آن با Effect می‌تواند Render اضافی و وضعیت ناسازگار بسازد. Effect برای همگام‌سازی با سامانه بیرونی است، نه راه اجباری برای هر محاسبه داخلی. مقاله Composition API در Vue هم تفاوت State، مقدار مشتق‌شده و اثر جانبی را با مدل دیگری توضیح می‌دهد؛ این تفکیک پیش از انتخاب ابزار Cache مفید است.

memo شرط و هزینه دارد

مرجع memo آن را بهینه‌سازی معرفی می‌کند، نه تضمین اجرا نشدن Component. Props با مقایسه متناسب بررسی می‌شوند و object یا تابع تازه می‌تواند نتیجه را عوض کند. State خود Component و Context مصرف‌شده هم مسیرهای تغییر دارند. اگر در هر Render آرایه تازه‌ای می‌سازید، memo روی مصرف‌کننده آن به تنهایی رابطه پایدار ایجاد نمی‌کند. ابتدا ببینید خروجی واقعاً یکسان مانده یا فقط از نظر محتوایی شبیه قبلی است.

Comparator سفارشی باید همه Props اثرگذار، از جمله تابع‌ها را درست بررسی کند. نادیده گرفتن تابعی که مقدار جدیدی را در Closure نگه داشته، می‌تواند رفتار قدیمی باقی بگذارد. مقایسه عمیق هم ممکن است از Render خود جزء گران‌تر باشد. Cache نباید درستی برنامه را به یک فرض شکننده وابسته کند. اگر حذف memo رفتار محصول را خراب می‌کند، احتمالاً مشکل اصلی در طراحی اثر یا State است و باید مستقل از بهینه‌سازی حل شود.

useMemo و useCallback را برای رابطه مشخص استفاده کنید

useMemo نتیجه محاسبه را میان Renderها با توجه به Dependencyها نگه می‌دارد. این ابزار هزینه محاسبه نخست را حذف نمی‌کند و درستی داده نباید به باقی ماندن Cache وابسته باشد. تابع محاسبه باید Pure باشد و وابستگی‌های واقعی کامل بیایند. حذف Dependency برای جلوگیری از اجرا، راه حل عملکرد نیست؛ داده قدیمی تولید می‌کند. اگر محاسبه کوچک و کم‌هزینه است، هزینه خواندن و نگهداری Cache را هم در تصمیم وارد کنید.

useCallback مرجع تابع را در رابطه با Dependencyها نگه می‌دارد و خود اجرای تابع را سریع‌تر نمی‌کند. وقتی تابع به جزء memoشده می‌رود، این پایداری می‌تواند مفید باشد. برای Event Handler ساده‌ای که چنین مصرف‌کننده‌ای ندارد، اضافه کردن آن ممکن است سود مشخصی نداشته باشد. نام Hook معیار انتخاب نیست؛ باید معلوم باشد کدام رابطه باعث Render یا اجرای غیرضروری می‌شود و چه چیزی با Cache ثابت می‌ماند.

نمونه کامل برای مشاهده Props پایدار

سه بخش زیر یک پروژه آموزشی کوچک می‌سازند. Node.js سازگار با Vite 7، از جمله شاخه 20.19 یا شاخه 22.12 به بالا در محدوده پشتیبانی آن، و npm لازم هستند. فایل package.json را در ریشه، index.html را کنار آن و main.jsx را در پوشه src بگذارید. سپس npm install و npm run dev را در همان ریشه اجرا و نشانی اعلام‌شده Vite را باز کنید. راهنمای Vite درباره محیط و مسیر شروع توضیح دارد؛ نمونه به API یا فایل تصویری خارجی نیاز ندارد.

{
  "name": "react-performance-demo",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
  "devDependencies": { "vite": "^7.0.0" }
}

HTML زیر محل Mount و CSS ساده نمونه را فراهم می‌کند. فایل main.jsx با module بارگذاری می‌شود و Vite JSX را پردازش می‌کند. فونت سیستمی است و Download جدا ندارد. کد صرفاً برای روشن شدن مرز Props و اندازه‌گیری React است؛ ظاهر آن یک صفحه محصول کامل محسوب نمی‌شود.

<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>نمونه عملکرد React</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:48rem;margin:2rem auto;padding:1rem}input,button{font:inherit;padding:.5rem;margin:.3rem}input:focus-visible,button:focus-visible{outline:3px solid #111;outline-offset:3px}ul{padding-inline-start:1.5rem}li{margin-block:.4rem}</style></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>

در main.jsx، داده آموزشی بیرون Component ساخته می‌شود. Query فهرست را فیلتر می‌کند و نتیجه با useMemo نگه داشته می‌شود. شمارنده مستقل، داده فهرست را تغییر نمی‌دهد. onSelect نیز مرجع پایدار دارد و به List که با memo پوشانده شده می‌رود. Profiler زمان Commitهای مربوط به این بخش را در Console ثبت می‌کند. پانصد ردیف در داده آموزشی عدد Benchmark نیست و زمان خروجی روی دستگاه‌های متفاوت می‌تواند فرق کند.

import React,{Profiler,memo,useCallback,useMemo,useState} from 'react';
import {createRoot} from 'react-dom/client';
const items=Array.from({length:500},(_,index)=>({id:index+1,name:`آیتم آموزشی ${index+1}`}));
const List=memo(function List({rows,onSelect}){return <ul>{rows.map(row=><li key={row.id}><button type="button" onClick={()=>onSelect(row.id)}>{row.name}</button></li>)}</ul>});
function record(id,phase,actualDuration,baseDuration){console.info({id,phase,actualDuration,baseDuration})}
function App(){
  const [query,setQuery]=useState(''),[counter,setCounter]=useState(0),[selected,setSelected]=useState(null);
  const filtered=useMemo(()=>items.filter(item=>item.name.includes(query.trim())),[query]);
  const onSelect=useCallback(id=>setSelected(id),[]);
  return <main><h1>مرز Memoization</h1><p>داده آموزشی است؛ زمان‌ها در Console ثبت می‌شوند.</p><label htmlFor="query">جست‌وجوی نام</label><input id="query" value={query} onChange={event=>setQuery(event.target.value)}/><p><button type="button" onClick={()=>setCounter(value=>value+1)}>شمارنده مستقل: {counter}</button></p><p role="status">نتیجه‌ها: {filtered.length}؛ انتخاب: {selected??'هیچ'}</p><Profiler id="results" onRender={record}><List rows={filtered} onSelect={onSelect}/></Profiler></main>;
}
createRoot(document.getElementById('root')).render(<App/>);

برای مشاهده رابطه، می‌توانید شمارنده مستقل را تغییر دهید و سپس Query را تغییر دهید؛ این دو کار داده فهرست را به شکل یکسان تغییر نمی‌دهند. در Code Review، پایداری آرایه و تابع را کنار مرز memo بخوانید. Profiler Callback فقط Log می‌نویسد و State دیگری را تغییر نمی‌دهد تا خودش زنجیره Update نسازد. نمونه React Compiler را نصب نکرده و برای توضیح Memoization دستی نوشته شده است. هیچ زمان یا نتیجه اجرای اندازه‌گیری‌شده‌ای برای آن ادعا نشده است.

React Compiler انتخاب‌های دستی را تغییر می‌دهد

راهنمای React Compiler توضیح می‌دهد که این ابزار می‌تواند Memoization را خودکار انجام دهد. فعال بودن آن در Build باید مشخص باشد؛ صرف استفاده از React جدید به معنی فعال شدن Compiler در همه پروژه‌ها نیست. وقتی Compiler دارید، افزودن دستی همه Hookهای Cache بدون بررسی ضرورت ممکن است کد را پیچیده کند. وقتی ندارید هم لازم نیست برای هر مقدار ساده Cache بنویسید. قرارداد درست State و تابع Pure در هر دو مسیر مهم است.

مهاجرت به Compiler باید با پشتیبانی ابزار و قواعد React هماهنگ باشد. کدی که در Render اثر جانبی نامناسب دارد یا State را خارج از مسیر مجاز تغییر می‌دهد، نباید با انتظار بهینه‌سازی خودکار رها شود. ابزار می‌تواند بعضی کارهای تکراری را مدیریت کند، اما Query سنگین، حجم داده و DOM بزرگ مسئله‌های مستقل‌اند. تصمیم درباره Cache باید در زمینه Build واقعی پروژه گزارش شود تا همکار بعدی نداند Memoization از کدام لایه آمده است.

حجم کار را با Split و Windowing کاهش دهید

lazy می‌تواند بارگذاری کد یک Component را تا زمان نیاز عقب بیندازد و به Suspense نیاز دارد. برای صفحه‌ای که آن بخش از ابتدا لازم است، Delay ممکن است انتظار تازه‌ای بسازد. مسیر خطای دانلود و تجربه بارگذاری را طراحی کنید. کاهش Bundle با کاهش رندر یک لیست تفاوت دارد. انتخاب Split باید با مسیر استفاده مرتبط باشد؛ تقسیم فایل‌ها بدون کاهش کد اولیه یا هزینه کاربر، بهینه‌سازی قابل توضیحی نیست.

در فهرست بسیار بزرگ، Virtualization تعداد عناصر حاضر را محدود می‌کند. این روش به اندازه ردیف، Scroll، Focus و دسترسی‌پذیری وابسته است و نام یک کتابخانه به تنهایی آن‌ها را حل نمی‌کند. Pagination یا محدود کردن نتیجه هم می‌تواند مناسب باشد. useDeferredValue می‌تواند اولویت به‌روزرسانی بخشی از رابط را تغییر دهد، اما هزینه محاسبه یا تعداد درخواست شبکه را خودکار حذف نمی‌کند. اگر کار CPU مستقل سنگین است، مقاله Web Workers مسیر دیگری معرفی می‌کند.

اصلاح را با رفتار محصول تحویل دهید

برای گزارش، سناریو، نسخه Build، داده و محدودیت مشاهده را بنویسید. سرعت بیشتر نباید با نتیجه قدیمی، خطای پنهان یا حذف اطلاعات ضروری به دست آمده باشد. Query جست‌وجو باید همچنان پاسخ درست بدهد و Focus پس از تغییر لیست حفظ شود. مقاله Responsive Images درباره هزینه تصویر و Server Components درباره مرز انتقال کد و داده زمینه بیشتری دارند. تغییر یک لایه را به عنوان حل همه هزینه‌های صفحه معرفی نکنید.

نقطه شروع مناسب یک تعامل پرتکرار است. مسیر انتظار را مشخص کنید، یک فرضیه کوچک درباره علت بنویسید و اصلاح را در همان محدوده انجام دهید. سپس توضیح دهید کدام هزینه حذف یا کم شده و چه محدودیتی باقی مانده است. اگر شواهد سود Memoization را نشان نمی‌دهند، کد ساده‌تر انتخاب قابل دفاعی است. بهینه‌سازی زمانی قابل نگهداری می‌ماند که هر Cache، مرز State یا Split دلیل مشخصی داشته باشد و رفتار صحیح محصول بدون آن هم روشن باشد.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!