React: تحسين الأداء: useMemo و useCallback
آخر تحديث: 2026-10-26
1. ما ستتعلمه
- تقوم دالة
useMemoبتخزين نتيجة عملية الحساب مؤقتًا - تقوم
useCallbackبتخزين مراجع الدوال مؤقتًا - تمنع ميزة React.memo إعادة العرض غير الضرورية
- متى نستخدمه، ومتى نسيء استخدامه
- مناهج تحليل الأداء
2. قصة صفحة تصفية البحث
العلاقة بين المكونات الثلاثة لتحسين الأداء
flowchart TD
A[Parent Component Rendering] --> B{Props It's changed?}
B -->|Yes| C[React.memo Intercept?]
B -->|No| D[Skip subcomponents ✅]
C -->|A rough comparison is roughly equal| D
C -->|Shallow Comparison Inequality| E{Function/The object is new.?}
E -->|Yes| F[useCallback/useMemo]
F --> G[Stable Citation → memo Effective ✅]
E -->|No| D
style F fill:#e8f5e9,stroke:#2e7d32
style D fill:#c8e6c9,stroke:#2e7d32
style G fill:#c8e6c9,stroke:#2e7d32
(1) المشكلة: إعادة الحساب مع كل إدخال
أنشأت أليس صفحة بحث عن المنتجات تضم 10,000 منتج يمكن تصفية النتائج منها:
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ❌ Question:Re-filter after each input 10,000 Items
const filtered = products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
المشكلة: في كل مرة يتم فيها إدخال قيمة من لوحة المفاتيح (setSearch)، يتسبب ذلك في تشغيل filter وsort مرة أخرى — ويتعين تصفية 10,000 سجل في كل مرة، مما يؤدي إلى بطء أداء الواجهة.
(2) حل useMemo
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ✅ useMemo:Only at search or sortBy Recalculate only when there is a change
const filtered = useMemo(() => {
console.log('Refilter...(Only when the search terms or sorting criteria change)')
return products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
}, [search, sortBy, products])
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
المزايا: لا يتم إعادة تشغيل عملية التصفية إلا عندما يتغير المدخل إلى search أو sortBy؛ أما التغييرات الأخرى في الحالة (مثل إعادة عرض المكون الأصلي) فلا تؤدي إلى تشغيل عملية التصفية، مما يؤدي إلى تحسن في الأداء بمقدار 10 إلى 100 ضعف.
3. useMemo: تخزين نتائج الحسابات مؤقتًا
| هوك | ما يجب تذكره | قيمة الإرجاع | عند تغير التبعيات |
|---|---|---|---|
useMemo |
نتيجة الحساب | القيمة المخزنة مؤقتًا | إعادة الحساب |
useCallback |
مراجع الوظائف | الوظائف المخزنة مؤقتًا | إنشاء وظائف جديدة |
React.memo |
نتيجة عرض المكون بأكمله | تخطي إعادة العرض | إعادة العرض عند تغيير الخصائص |
(1) قواعد النحو الأساسية
const memoizedValue = useMemo(() => {
return Costly Computations()
}, [Dependency1, Dependency2])
| القسم | الوصف |
|---|---|
() => Computation |
دالة يجب تخزينها مؤقتًا وتُرجع نتيجة الحساب |
[Dependencies] |
إعادة الحساب عند تغير التبعيات |
memoizedValue |
النتائج المخزنة مؤقتًا (تُرجع النتيجة السابقة مباشرةً إذا لم تتغير التبعيات) |
(2) السيناريوهات التي ينطبق عليها ذلك
// Scene 1:Data Conversion(Filter、Sort、Grouping)
const activeUsers = useMemo(
() => users.filter(u => u.isActive).sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
// Scene 2:Format(Numbers、Date、String Concatenation)
const formattedPrice = useMemo(
() => new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price),
[price]
)
// Scene 3:Derived State(Calculating New Data from Existing Data)
const statistics = useMemo(() => ({
total: orders.length,
paid: orders.filter(o => o.status === 'paid').length,
amount: orders.reduce((sum, o) => sum + o.amount, 0),
avgAmount: orders.length ? orders.reduce((sum, o) => sum + o.amount, 0) / orders.length : 0
}), [orders])
الناتج:
...
▶ مثال: مقارنة الأداء
// ============================================
// Example:useMemo vs no useMemo Performance Comparison
// ============================================
function ExpensiveCalc({ num, unrelated }) {
// ❌ None useMemo:Executed on every render
const slowResult1 = slowFibonacci(num)
// ✅ With useMemo: num If it hasn't changed, don't recalculate it.
const slowResult2 = useMemo(() => slowFibonacci(num), [num])
return (
<div>
<p>num={num} The Fibonacci sequence</p>
<p>Without useMemo: {slowResult1}(Rendering calculations each time)</p>
<p>With useMemo: {slowResult2}(Only at num Calculate when changes occur)</p>
<p>Unrelated Status:{unrelated}(Click the button to change this value)</p>
</div>
)
}
function slowFibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1, c
for (let i = 2; i <= n; i++) { c = a + b; a = b; b = c }
console.log('fibonacci Calculation!n=' + n)
return b
}
// Click to Make a Difference unrelated → without useMemo recalculates,with useMemo skips
الناتج:
Fibonacci(n): بدون useMemo يُعاد الحساب في كل عملية عرض، ومع useMemo يُتخطى إلا عند تغير حالة غير ذات صلة. وحدة التحكم: "fibonacci calculated! n=X"
4. useCallback: حفظ مراجع الدوال
(1) قواعد النحو الأساسية
const memoizedFn = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps) تعادل useMemo(() => fn, deps) — فما يتم تخزينه هو الدالة نفسها، وليس نتيجة الدالة.
(2) لماذا يُعد useCallback ضروريًا؟
function Parent() {
const [count, setCount] = useState(0)
// ❌ Every time Parent Rendering,handleClick These are all new functions
// resulting in Child 's React.memo Failure
const handleClick = () => {
console.log('Clicked')
}
// ✅ useCallback:handleClick Citation unchanged
// Child 's React.memo Works properly(Skip Re-rendering)
const handleClick = useCallback(() => {
console.log('Clicked')
}, []) // Empty Dependency:This function will never change.
}
(3) React.memo: منع إعادة عرض المكونات الفرعية
▶ جرّب الكود5. متى يجب استخدامه، ومتى لا يجب استخدامه
(1) متى يتم استخدامه
| السيناريو | العنصر الجاذب | السبب |
|---|---|---|
| تصفية/فرز البيانات الضخمة | useMemo |
تجنب البحث في كميات كبيرة من البيانات في كل عملية عرض |
| الحسابات المعقدة (الرياضيات/التنسيق) | useMemo |
لا داعي لإعادة الحساب إذا بقيت النتيجة كما هي |
| تمرير المعلمات إلى مكون فرعي يستخدم React.memo | useCallback |
الحفاظ على استقرار مراجع الدوال لضمان عمل React.memo |
| التبعيات في دالة useEffect | useCallback |
منع تنفيذ دالة useEffect عدة مرات بسبب التغييرات في مراجع الدالة |
(2) الحالات التي لا تكون فيها هناك حاجة إليها
// ❌ Not necessary:Simple Calculations
const doubled = useMemo(() => count * 2, [count])
// ✅ Write it directly:count * 2 It's already pretty fast as it is
const doubled = count * 2
// ❌ Not necessary:Handle events without passing them to child components
const handleClick = useCallback(() => {
console.log('Click')
}, [])
// ✅ Just write it down.
const handleClick = () => console.log('Click')
// ❌ Not necessary:The child component isn't being used. React.memo
// useCallback It's been packaged, but no one is buying it."Stable Citation"
(3) تكلفة الإساءة
// useMemo and useCallback It involves expenses(Relies heavily on arrays)
// Abuse = Introducing Additional Comparison Logic,Slower than if it weren't optimized
// Rule of thumb:
// 1. First, let's write the version without optimization.
// 2. Feeling some lag(fps < 60)Use again
// 3. use React DevTools Profiler Measurement
6. مثال كامل: قائمة الرسائل في تطبيق الدردشة
▶ عرض المثال القابل للتشغيل (112 سطرًا)
الناتج:
... ('fibonacci Calculation!n=' + n)
الناتج المتوقع: قائمة محادثة تحتوي على 100 رسالة. عند النقر على «زيادة عدد الرسائل غير المقروءة»، لن يتم إعادة عرض قائمة الرسائل (لن يظهر أي ناتج في وحدة التحكم) بسبب
useCallbackوReact.memo؛ بل سيتغير العدد فقط.
▶ المثال 2: تمنع دالة useCallback دالة useEffect من التشغيل عدة مرات
▶ جرّب الكود
الناتج:
مربع بحث + ترقيم صفحات. إدخال "React" → 3 نتائج ("Results React - Page 1" وغيرها). يمنع useCallback الحلقة اللانهائية في useEffect. أزرار Prev/Next.
الناتج:
العنوان الفرعي: "🔍 Search(useCallback Demo)". يعرض: "🔍 Search(useCallback Demo)". الحالة: query (setter: setQuery)، results (setter: setResults)، page (setter: setPage). زر: setPage(p => p + 1)}>Next Page. الإدخال: Enter a search term.... القائمة: {r.title}. يتولى useEffect إدارة الآثار الجانبية. يُخزن useCallback مرجع دالة المعالجة. جلب بيانات غير متزامن/حالة التحميل
▶ المثال 3: استخدام useMemo لتنفيذ حسابات القوائم الافتراضية
function VirtualList({ items, itemHeight = 40, visibleHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleCount = Math.ceil(visibleHeight / itemHeight)
const totalHeight = useMemo(() => items.length * itemHeight, [items.length, itemHeight])
const { startIndex, endIndex, offsetY } = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight)
const end = Math.min(start + visibleCount + 1, items.length)
return { startIndex: start, endIndex: end, offsetY: start * itemHeight }
}, [scrollTop, itemHeight, visibleCount, items.length])
const visibleItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex])
return (
<div onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
style={{ height: visibleHeight, overflowY: 'auto', border: '1px solid #ddd', borderRadius: 4 }}>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ position: 'absolute', top: offsetY, width: '100%' }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight, padding: '0 12px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
Row {startIndex + i}: {item}
</div>
))}
</div>
</div>
</div>
)
}
الناتج:
التمرير الافتراضي: لا تُعرض إلا الصفوف المرئية (نحو 10 صفوف من 1000+). التمرير → تُستبدل الصفوف تباعًا. الارتفاع الكلي = items × 40px. يحسب useMemo النطاق المرئي.
الناتج:
يعرض: "items.slice(startIndex, endIndex), [items, startIndex, endIn". الحالة: scrollTop (setter: setScrollTop). يحسّن useMemo حساب الرسم
▶ المثال 4: تحسين أداء القوائم باستخدام React.memo + useCallback
▶ جرّب الكودالناتج:
قائمة منتجات (Keyboard $79، Mouse $49، Monitor $399، Headphones $129) مع إمكانية التصفية. "Remove" يحذف العنصر. React.memo + useCallback = لا يُعاد العرض إلا للعناصر التي تغيرت.
الناتج:
العرض: ...
▶ المثال 5: متى لا ينبغي استخدام useMemo — مقارنة مع الأنماط السيئة
function BadExample({ count }) {
// ❌ Simple calculations are not required useMemo——Create memo Expenses > The Calculation Itself
const doubled = useMemo(() => count * 2, [count])
// ✅ Direct Calculation
const doubledOk = count * 2
// ❌ Each render creates a new object as useMemo Dependency
const config = useMemo(() => ({ count }), [count])
const badResult = useMemo(() => config.count * 3, [config]) // config A new reference each time!
// ✅ Using Primitive Types Directly as Dependencies
const goodResult = useMemo(() => count * 3, [count])
return <p>Doubled: {doubledOk}, Tripled: {goodResult}</p>
}
function GoodExample({ items, query }) {
// ✅ Filter/Sorting large amounts of data useMemo
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
).sort((a, b) => a.name.localeCompare(b.name))
}, [items, query])
// ✅ Complex objects passed to child components use useMemo
const chartData = useMemo(() => ({
labels: filtered.map(i => i.name),
values: filtered.map(i => i.price),
}), [filtered])
return (
<div>
<p>Found: {filtered.length} items</p>
<Chart data={chartData} />
</div>
)
}
الناتج:
❌ استخدام useMemo مع count*2 (التكلفة > الحساب نفسه). ❌ استخدام كائن جديد كاعتماد يكسر memo. ✅ استخدم useMemo للتصفية/الترتيب على نطاق كبير وبيانات الرسوم البيانية المشتقة.
❓ أسئلة شائعة
useMemo وuseCallback دائمًا على تحسين الأداء؟dependencies في كل عملية عرض. وقد يؤدي الإفراط في استخدامهما إلى إبطاء الأداء فعليًّا. التوصية الرسمية لـ React: ابدأ بكتابة النسخة غير المُحسَّنة، ولا تقم بالتحسين إلا عندما تلاحظ حدوث تأخير. القاعدة العامة: استخدمهما فقط للحسابات التي يتم تمريرها إلى مكون فرعي من نوع React.memo أو تلك التي تتطلب وقتًا إضافيًا يزيد عن 1 مللي ثانية.useMemo والمتغير العادي؟useMemo «ذاكرة» — فإذا لم تتغير التبعيات، فإنه يستخدم القيمة المخزنة في ذاكرة التخزين المؤقت مباشرةً دون إعادة تقييم التعبير. أما المتغير العادي فيُعاد تقييمه عند كل عملية عرض. ومع ذلك، يحتاج useMemo إلى مقارنة مصفوفة التبعيات أثناء العرض، لذا فإن «الحسابات البسيطة تكون في الواقع أبطأ مع useMemo.»useCallback للحفاظ على مرجع الدالة؟useEffect. إذا لم ينطبق أي من هذين الشرطين، فيمكنك ببساطة كتابة الدالة مباشرةً؛ ولا داعي لاستخدام useCallback.useMemo وuseCallback؟useMemo(() => value, deps) بتخزين نتيجة (قيمة) عملية حسابية في ذاكرة التخزين المؤقت، بينما يقوم useCallback(() => fn, deps) بتخزين مرجع الدالة في ذاكرة التخزين المؤقت. وفي الواقع، فإن useCallback(fn, deps) يعادل useMemo(() => fn, deps). استخدم useCallback عندما تحتاج إلى تخزين دالة تم تمريرها إلى مكون فرعي؛ واستخدم useMemo عندما تحتاج إلى تخزين نتيجة عملية حسابية.📖 ملخص
useMemoتخزين نتائج الحساب مؤقتًا؛ إذا بقيت التبعيات دون تغيير، فلا تُعاد الحساباتuseCallbackتخزين مراجع الدوال مؤقتًا لمنع إعادة عرض المكونات الفرعية بالتزامن مع React.memoReact.memoإجراء مقارنة سطحية بين قيم الخصائص (props)؛ إذا لم تتغير قيم الخصائص، يتم تخطي عرض المكون الفرعي- لا تقم بالتحسين في مرحلة مبكرة جدًا — اكتب أولاً كودًا يعمل بشكل جيد، ولا تقم بتحسينه إلا عندما يبدأ في التعطل.
- تحديد معوقات الأداء باستخدام أداة تحليل الأداء في React DevTools
📝 تمارين
- المشكلة الأساسية (الصعوبة ⭐): أنشئ مكونًا
PrimeCalculatorيتلقى رقمًا n كمدخل ويحسب العدد الأولي رقم n. استخدمuseMemoلتخزين النتيجة مؤقتًا حتى لا يتم إعادة حسابها في كل مرة يتم فيها إدخال قيمة جديدة. - تمرين متقدم (مستوى الصعوبة ⭐⭐): أنشئ مكونًا
ProductTableيحتوي على 500 سجل منتج ويدعم التصفية حسب الفئة والفرز حسب السعر. استخدمuseMemoلتحسين منطق التصفية والفرز، واستخدمuseCallbackمعReact.memoلتحسين عرض القائمة. - التحدي (الصعوبة: ⭐⭐⭐): قم بإنشاء مكون
DataDashboardيقوم بتجميع 10,000 سجل طلب عبر ثلاثة أبعاد مختلفة (الوقت، والمنطقة، والفئة)، واستخدمuseMemoلتنفيذ التخزين المؤقت الهرمي (بحيث يتم إعادة حساب الطبقات ذات الصلة فقط عند تغير الأبعاد).