تحسين أداء الواجهة
واجهة تفتح بسرعة وتستجيب فورًا ⏱ نحو 40 دقيقة.
- تقيس الأداء بمؤشرات
Web Vitalsوأدوات المطوّر. - تفسّر متى يُعاد رسم المكوّن وكيف تمنع الرسم غير الضروري.
- تستخدم
memoوuseMemoوuseCallbackفي موضعها. - تقلل الطلبات بالتأجيل، والحجم بالتقسيم وتحسين الصور.
١قِس قبل أن تحسّن
التحسين بلا قياس يضيّع الوقت ويعقّد الكود. ابدأ بسؤال: ما الذي يشعر به المستخدم بطيئًا؟ ثم قِسه. Google تقيس تجربة المستخدم بثلاثة مؤشرات Core Web Vitals:
| المؤشر | يقيس | الهدف الجيد | أشهر أسبابه |
|---|---|---|---|
| LCP أكبر رسم للمحتوى | متى ظهر أكبر عنصر (صورة أو عنوان) | أقل من 2.5 ثانية | حزمة JavaScript كبيرة، صور ضخمة، خادم بطيء |
| INP التفاعل حتى الرسم التالي | سرعة استجابة الصفحة للنقر والكتابة | أقل من 200 ملّي ثانية | حسابات ثقيلة أو إعادة رسم كثيرة |
| CLS إزاحة التخطيط | قفز العناصر أثناء التحميل | أقل من 0.1 | صور بلا أبعاد، محتوى يُحقن فوق الموجود |
الأدوات: تبويب Lighthouse و Performance في أدوات مطوّر Chrome، وإضافة React DevTools وفيها Profiler يُظهر أي مكوّن أُعيد رسمه وكم استغرق.
٢متى يُعاد رسم المكوّن؟
React يعيد رسم المكوّن حين تتغير حالته، وحين يُعاد رسم أبيه، حتى لو لم تتغير خصائصه. هذا سريع عادة، لكنه يصبح مشكلة مع مكوّنات ثقيلة أو قوائم طويلة. هنا كل مكوّن يطبع في Console عند رسمه. اكتب في الحقل وراقب:
import { useState } from 'react'; function Header() { console.log('رُسم Header'); return <h3>متجر الكلية</h3>;} function ProductList({ items }) { console.log('رُسمت القائمة', items.length); return <ul>{items.map(p => <li key={p}>{p}</li>)}</ul>;} export default function App() { const [note, setNote] = useState(''); const items = ['قلم', 'دفتر', 'حقيبة']; return ( <div> <Header /> <input value={note} onChange={e => setNote(e.target.value)} placeholder="ملاحظة" /> <ProductList items={items} /> </div> );}
كل حرف يُعيد رسم Header والقائمة رغم أنهما لا يعتمدان على الملاحظة إطلاقًا.
٣memo و useCallback و useMemo
| الأداة | تحفظ | متى |
|---|---|---|
React.memo(Comp) | المكوّن: لا يُعاد رسمه إن لم تتغير خصائصه | مكوّن ثقيل يُرسم كثيرًا بالخصائص نفسها |
useMemo(() => قيمة, [deps]) | نتيجة حساب | حساب مكلف، أو مصفوفة/كائن يُمرَّر لمكوّن memo |
useCallback(fn, [deps]) | الدالة نفسها | دالة تُمرَّر لمكوّن memo |
لماذا نحتاج الأخيرين؟ لأن memo يقارن الخصائص بالمرجع: المصفوفة ['قلم', …] والدالة () => … تُنشآن من جديد مع كل رسم، فتبدوان «مختلفتين» وتُبطلان memo. هنا النسخة المحسّنة؛ اكتب وراقب Console:
import { useState, memo, useMemo, useCallback } from 'react'; const ProductList = memo(function ProductList({ items, onPick }) { console.log('رُسمت القائمة'); return <ul>{items.map(p => <li key={p} onClick={() => onPick(p)}>{p}</li>)}</ul>;}); export default function App() { const [note, setNote] = useState(''); const [picked, setPicked] = useState(''); const items = useMemo(() => ['قلم', 'دفتر', 'حقيبة'], []); const onPick = useCallback((p) => setPicked(p), []); return ( <div> <input value={note} onChange={e => setNote(e.target.value)} placeholder="ملاحظة" /> <p>اخترت: {picked}</p> <ProductList items={items} onPick={onPick} /> </div> );}
الآن الكتابة لا تعيد رسم القائمة؛ تُرسم مرة واحدة فقط. والنقر على عنصر يعمل لأن onPick ثابتة.
٤حساب ثقيل: useMemo
هنا تصفية وترتيب 20 ألف منتج. بدون useMemo يُعاد الحساب مع كل رسم، حتى عند تبديل السمة (الذي لا علاقة له بالقائمة). مع useMemo لا يُعاد إلا إن تغيّر البحث:
import { useState, useMemo } from 'react'; const PRODUCTS = Array.from({ length: 20000 }, (_, i) => ({ id: i, name: 'منتج ' + i, price: (i * 37) % 500 })); export default function App() { const [q, setQ] = useState(''); const [dark, setDark] = useState(false); const result = useMemo(() => { const t0 = performance.now(); const r = PRODUCTS.filter(p => p.name.includes(q)).sort((a, b) => a.price - b.price); console.log(`حُسبت القائمة في ${(performance.now() - t0).toFixed(1)} ms`); return r; }, [q]); return ( <div style={{ background: dark ? '#222' : '#fff', color: dark ? '#eee' : '#000', padding: 8 }}> <input value={q} onChange={e => setQ(e.target.value)} placeholder="ابحث برقم" /> <button onClick={() => setDark(!dark)}>بدّل السمة</button> <p>{result.length} نتيجة · الأرخص: {result[0]?.name}</p> </div> );}
اضغط «بدّل السمة» مرات: لا يظهر سطر حساب جديد في Console. احذف useMemo في «جرّب وعدّل» لترى الحساب يتكرر مع كل نقرة.
٥التأجيل Debounce: لا ترسل طلبًا مع كل حرف
بحث يرسل طلبًا للخادم مع كل حرف يكتبه المستخدم = عشرات الطلبات لكلمة واحدة. التأجيل ينتظر حتى يتوقف المستخدم عن الكتابة فترة قصيرة (مثل 400 ملّي ثانية) ثم يرسل مرة واحدة. Hook صغير يكفي:
import { useState, useEffect } from 'react';import { useDebounce } from './useDebounce.js'; export default function App() { const [q, setQ] = useState(''); const dq = useDebounce(q, 400); const [sent, setSent] = useState(0); useEffect(() => { if (!dq) return; setSent(n => n + 1); console.log('طلب بحث عن:', dq); // هنا يكون fetch }, [dq]); return ( <div> <input value={q} onChange={e => setQ(e.target.value)} placeholder="اكتب بسرعة…" /> <p>طلبات أُرسلت: <b className="sent">{sent}</b></p> </div> );}
import { useState, useEffect } from 'react'; export function useDebounce(value, ms) { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); // كل حرف جديد يلغي المؤقت السابق }, [value, ms]); return v;}
اكتب كلمة كاملة بسرعة: يُرسل طلب واحد بعد توقفك، لا طلب لكل حرف.
٦حجم الحزمة والتحميل
| التقنية | كيف | تؤثر في |
|---|---|---|
| تقسيم الكود | lazy(() => import('./Admin.jsx')) للصفحات الثقيلة والنادرة | LCP |
| مكتبات أخف | استورد الدالة لا المكتبة كاملة؛ وافحص الحجم قبل التثبيت | LCP |
| الصور | صيغ WebP/AVIF، وأبعاد مناسبة، و loading="lazy" لما تحت الشاشة | LCP |
| أبعاد ثابتة | width و height للصور والإطارات | CLS |
| القوائم الطويلة | الترقيم، أو «النافذة» Virtualization: رسم الظاهر فقط (مكتبات مثل react-window) | INP |
| الضغط والتخزين | gzip/brotli، و Cache-Control للأصول المجزّأة (الدرس 21) | LCP |
<img src="/img/hero-800.webp" alt="منتجات الأسبوع" width="800" height="400" // يمنع قفز الصفحة (CLS) loading="lazy" decoding="async" /> // لا تُحمَّل حتى تقترب من الشاشة
توقّع وتحقّق
أي مؤشر يقيس سرعة استجابة الصفحة للنقر؟
Interaction.متى يُعاد رسم مكوّن ابن عادة؟
لماذا قد لا يعمل memo مع الخاصية onClick={() => …}؟
useCallback.فائدة التأجيل debounce في البحث:
الخلاصة
- قِس أولًا:
LCPللتحميل، وINPللاستجابة، وCLSللثبات. - الابن يُرسم مع أبيه؛
memoيمنع ذلك إن لم تتغير الخصائص. useMemoللقيم والحسابات، وuseCallbackللدوال الممررة لمكوّنmemo.debounce: طلب واحد بعد توقف الكتابة.lazyللصفحات الثقيلة، وصور بأبعاد وloading="lazy".
تحدٍّ برمجي: قائمة لا تُعاد بلا داع
متقدمالتطبيق فيه عدّاد وقائمة طلاب ثقيلة الرسم. كل نقرة على العداد تعيد رسم القائمة دون داع. حسّنه دون تغيير ما يظهر.
- لفّ
StudentListبـmemo. - اجعل المصفوفة
studentsوالدالةonSelectثابتتين بين الرسمات (useMemo و useCallback). - بعد ذلك: النقر على العداد لا يطبع «رُسمت القائمة» من جديد، والنقر على طالب ما زال يعمل.
👀 اعرض الناتج المطلوب (وجرّبه)
- الكود يعمل بلا أخطاء في Console
- تظهر القائمة بثلاثة طلاب
- ثلاث نقرات على العداد لا تعيد رسم القائمة
- النقر على طالب ما زال يعمل
- يستخدم memo و useCallback
💡 تلميح 1
const StudentList = memo(function StudentList(…) {…});💡 تلميح 2
useMemo(() => [...], [])، والدالة: useCallback((s) => setSelected(s), []).حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import { useState, memo, useMemo, useCallback } from 'react'; const StudentList = memo(function StudentList({ students, onSelect }) { console.log('رُسمت القائمة'); return <ul>{students.map(s => <li key={s} onClick={() => onSelect(s)}>{s}</li>)}</ul>;}); export default function App() { const [count, setCount] = useState(0); const [selected, setSelected] = useState(''); const students = useMemo(() => ['ريم', 'فهد', 'سارة'], []); const onSelect = useCallback((s) => setSelected(s), []); return ( <div> <button className="inc" onClick={() => setCount(count + 1)}>العداد: {count}</button> <p className="sel">المختار: {selected}</p> <StudentList students={students} onSelect={onSelect} /> </div> );}