الدليل الشامل لتحسين أداء React
من memo إلى مكونات الخادم — دليل عملي لجعل تطبيق React أسرع دون هندسة مفرطة.
تحسين الأداء في React لديه مشكلة سمعة. اسأل عشرة مطورين كيفية جعل تطبيق React أسرع، وثمانية سيقولون "استخدم useMemo وuseCallback لكل شيء." الاثنان الباقيان سيقولان "التحسين المبكر هو أصل كل شر" ويمشيان بعيداً. كلا المجموعتين مخطئتان، وكلاهما ينتج تطبيقات إما معقدة دون داعٍ أو بطيئة دون داعٍ.
الواقع أكثر دقة. نموذج العرض في React فعال لمعظم الحالات خارج الصندوق، لكن هناك أنماطاً مفهومة جيداً حيث يقصر. هذا الدليل يغطي كل تقنية تحسين مهمة في React — كيف تعمل، ومتى تساعد، والأهم من ذلك، ومتى تضر. الهدف ليس جعلك تلجأ إلى memo في كل مكان. إنه إعطاؤك نموذجاً عقلياً للأداء يجعل الاختيار الصحيح واضحاً في كل موقف.
فهم متى يعيد React العرض
قبل تحسين أي شيء، تحتاج إلى فهم ما يسبب إعادة العرض. React يعيد عرض المكون عندما تتغير حالته، أو عندما يعيد أصله العرض، أو عندما تتغير قيمة سياق يستهلكها. يبدو ذلك بسيطاً، لكن التأثير المتتالي هو حيث تختبئ مشكلات الأداء.
عندما يعيد المكون الأصلي العرض، كل مكون فرعي يعيد العرض افتراضياً — حتى لو لم تتغير خصائص المكون الفرعي. React يفعل هذا لأنه لا يمكنه معرفة ما إذا كان المكون الفرعي يعتمد على حالة الأصل دون تنفيذ دالة العرض للمكون الفرعي. هذا ليس خللاً؛ إنه اختيار تصميمي يحافظ على نموذج المصالحة في React بسيطاً وقابلاً للتنبؤ. لكنه يعني أن تغيير حالة في مكون عالي المستوى يمكن أن يؤدي إلى إعادة عرض عبر عشرات أو مئات الأحفاد.
البصيرة الرئيسية هي أن إعادة العرض ليست هي نفسها تحديث DOM. React يقارن مخرجات DOM الافتراضية (JSX) بالمخرجات السابقة ويلتزم فقط بالاختلافات في DOM الحقيقي. يمكن لمكون إعادة العرض ألف مرة دون تغيير DOM واحد. التكلفة هي في تنفيذ جافا سكريبت — إنشاء كائنات DOM افتراضية، تشغيل hooks، ومقارنة الأشجار. لأشجار المكونات الصغيرة، هذه التكلفة لا تذكر. للقوائم الكبيرة، أو الرسوم البيانية المعقدة، أو المكونات التي تقوم بحسابات مكلفة، تتراكم بسرعة.
// Every keystroke in this input re-renders the entire tree
function SearchPage() {
const [query, setQuery] = useState("");
return (
<div>
<SearchInput value={query} onChange={setQuery} />
<SearchResults query={query} />
<Sidebar>
<FilterPanel />
<RecentSearches />
</Sidebar>
</div>
);
}
// Without optimization, FilterPanel and RecentSearches
// re-render on every keystroke, even though nothing
// they depend on has changed.الخطوة الأولى في أي تحقيق أداء هي تحديد ما إذا كان لديك مشكلة فعلاً. التحسين المبكر يضيف تعقيداً دون فائدة قابلة للقياس. حلل أولاً، حسن ثانياً — لكن عندما تحدد عنق زجاجة، تقنيات هذا الدليل هي الأدوات التي تلجأ إليها.
التخزين المؤقت: memo وuseMemo وuseCallback
التخزين المؤقت هو تقنية تحسين React الأكثر مناقشة والأكثر إساءة استخدام. الفكرة الأساسية بسيطة: إذا كانت دالة تنتج نفس المخرجات لنفس المدخلات، خزن النتيجة مؤقتاً وتجاوز الحساب في الاستدعاءات اللاحقة. React توفر ثلاث أدوات تخزين مؤقت، كل منها يخدم غرضاً مختلفاً.
React.memo يلف مكوناً ويمنع إعادة العرض عندما لم تتغير خصائصه (باستخدام المقارنة السطحية). هو الأكثر فعالية للمكونات الطرفية التي تستقبل نفس الخصائص بشكل متكرر — فكر في عناصر القائمة، وعناصر المخططات، أو أي مكون نقي يعتمد مخرجاته على خصائصه فقط.
import { memo } from "react";
const ExpenseRow = memo(function ExpenseRow({ label, amount }: {
label: string;
amount: number;
}) {
return (
<tr>
<td>{label}</td>
<td className={amount < 0 ? "text-red-500" : "text-green-500"}>
${amount.toFixed(2)}
</td>
</tr>
);
});
// Now ExpenseRow only re-renders when label or amount changes.
// Without memo, it re-renders every time the parent re-renders.useMemo يخزن نتيجة عملية حسابية بين عمليات العرض مؤقتاً. إنه مفيد عندما يقوم مكون بحساب مكلف في كل عرض — تصفية مصفوفة كبيرة، تنسيق بيانات، أو تشغيل تحويل معقد. بدون useMemo، هذا العمل يتكرر في كل إعادة عرض حتى لو لم تتغير المدخلات.
function Dashboard({ transactions, filter }: Props) {
// Without useMemo: this runs on every render
const visibleTransactions = useMemo(
() => transactions
.filter(t => t.date >= filter.start && t.date <= filter.end)
.sort((a, b) => b.amount - a.amount),
[transactions, filter]
);
// Without useMemo, filtering and sorting 10,000 items
// happens on every keystroke in any input on the page.
return <TransactionList items={visibleTransactions} />;
}useCallback هو نفسه useMemo لكن للدوال. يعيد نسخة مخزنة مؤقتاً من دالة رد التي تتغير فقط عندما تتغير تبعياتها. هذا مهم لأن الدوال المعرفة في جسم العرض تُعاد إنشاؤها مع كل عرض، مما يكسر المقارنة السطحية التي يستخدمها memo.
هذه هي القاعدة الحاسمة التي تخطئ فيها معظم الأدلة: لا تلف كل شيء في useMemo وuseCallback. كل استدعاء تخزين مؤقت له تكلفة — تخزين مصفوفة التبعيات، مقارنتها في كل عرض، وتخصيص ذاكرة الإغلاق. تطبيق التخزين المؤقت على عمليات رخيصة يجعل تطبيقك أبطأ، وليس أسرع. المبدأ التوجيهي بسيط: خزن مؤقتاً فقط إذا قست مشكلة، أو إذا كان الحساب مكلفاً بشكل واضح (تحولات بيانات معقدة، عمليات تكرارية، عرض قوائم كبيرة).
أغلى تخزين مؤقت هو ذلك الذي كان يجب ألا يُكتب. حلل أولاً. لف ثانياً. كل تجريد له تكلفة، والتخزين المؤقت هو تجريد فوق دورة العرض.
النمط المعادي الشائع هو لف كل شيء في memo بشكل دفاعي، أملاً في منع مشكلات الأداء. هذا لا يعمل. إنه يضيف عبئاً إلى كل مقارنة عرض، ويضخم الحزمة بكود إضافي، ويجعل قاعدة الكود أصعب في التفكير. أضف التخزين المؤقت كتحسين مستهدف بعد أن يؤكد التحليل أنه يساعد، وليس كنمط افتراضي.
تقسيم الكود مع React.lazy وSuspense
حجم الحزمة هو البعد الأدائي الأكثر تجاهلاً في تطبيقات React. المطورون يهوسون بتحسين العرض بينما يشحنون 500 كيلوبايت من جافا سكريبت يجب على كل مستخدم تنزيلها وتحليلها وتنفيذها قبل أن يروا أي شيء. تقسيم الكود يحل هذا عن طريق تقسيم حزمتك إلى أجزاء تُحمّل عند الطلب.
React.lazy يتيح لك عرض مكون مستورد ديناميكياً مثل مكون عادي. مقترناً بـ Suspense، يمكنك إظهار واجهة مستخدم احتياطية أثناء تحميل القطعة. الفوز في الأداء ذو شقين: الحزمة الأولية أصغر، لذا تتحمّل الصفحة أسرع، والمستخدمون يدفعون فقط ثمن الكود الذي يستخدمونه فعلاً.
import { lazy, Suspense } from "react";
const AnalyticsDashboard = lazy(
() => import("./AnalyticsDashboard")
);
const DataExportPanel = lazy(
() => import("./DataExportPanel")
);
function App() {
const [showAnalytics, setShowAnalytics] = useState(false);
return (
<div>
<button onClick={() => setShowAnalytics(true)}>
View Analytics
</button>
<Suspense fallback={<div>Loading...</div>}>
{showAnalytics && <AnalyticsDashboard />}
</Suspense>
</div>
);
}
// AnalyticsDashboard and its dependencies are only loaded
// when the user clicks the button, not on initial page load.أفضل المرشحين للتحميل البطيء هم المكونات على مستوى المسار، ومكتبات التصور الثقيلة (الرسوم البيانية، المخططات، الخرائط)، ومحررات النص الغنية، وأي مكون أسفل الجزء المرئي من الصفحة أو يتم تشغيله بتفاعل المستخدم. قاعدة جيدة: إذا أضاف مكون أكثر من 20 كيلوبايت إلى الحزمة ولم يكن مرئياً في العرض الأولي، يجب تحميله بطيئاً على الأرجح.
سلوك Suspense المحسّن في React 19 يجعل تقسيم الكود أكثر عملية. جلب البيانات داخل حدود Suspense أصبح الآن مدمجاً بالكامل مع دورة حياة العرض، مما يلغي مشكلة الشلال حيث كان عليك تحميل القطعة أولاً ثم جلب بياناتها. مقترناً بـ Hook use() الجديد لقراءة الوعود، أصبحت الحدود بين حالات التحميل أنظف وأكثر قابلية للتكوين.
التمرير الافتراضي وتحسين القوائم
عرض القوائم الكبيرة هو مشكلة الأداء الأكثر شيوعاً في تطبيقات React الحقيقية. قائمة من 10,000 عنصر تعمل بشكل جيد في التطوير وتنهار في الإنتاج لأن React يجب أن ينشئ ويوافق 10,000 عقدة DOM افتراضية في كل عرض. محرك تخطيط المتصفح بعد ذلك يجب أن يحسب مواقع 10,000 عقدة DOM. النتيجة هي واجهة مستخدم مجمدة ومستخدم محبط.
التمرير الافتراضي يحل هذا بعرض العناصر المرئية فقط في إطار العرض، بالإضافة إلى مخزن مؤقت صغير أعلى وأسفل. بينما يمرر المستخدم، العناصر خارج إطار العرض تُفكّ والعناصر الجديدة تُركّب. يبقى DOM صغيراً — عادةً 20-30 عقدة بغض النظر عن حجم القائمة — وتكلفة مصالحة React تبقى ثابتة.
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height
overscan: 5, // render 5 extra items off-screen
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: virtualItem.size,
width: "100%",
}}
>
<ItemRenderer item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
// Only ~25-30 rows are rendered regardless of list size.
// 100,000 items? Same render cost.وراء التمرير الافتراضي، أداء القائمة يعتمد على المفاتيح المستقرة. استخدام فهرس المصفوفة كمفتاح يتسبب في تحديد React الخاطئ لأي العناصر تغيرت، مما يؤدي إلى فك وتركيب غير ضروريين لعقد DOM. استخدم معرفاً فريداً من بياناتك — معرف العنصر — كمفتاح. هذا يسمح لـ React بإعادة استخدام عقد DOM عند إعادة ترتيب العناصر أو إدراجها أو إزالتها، وهو أرخص بكثير من تدميرها وإعادة إنشائها.
- استخدم التمرير الافتراضي لأي قائمة من المحتمل أن تتجاوز 200-500 عنصر. الحد الأدنى يعتمد على تعقيد العنصر، لكن 500 صف من التعقيد المعتدل هو حيث تبدأ بملاحظة التقطيع.
- استخدم دائماً مفاتيح مستقرة وفريدة مستمدة من بياناتك (معرفات العناصر). مفاتيح الفهرس هي الملاذ الأخير للقوائم الثابتة التي لا تتغير أبداً.
- حافظ على مكونات عناصر القائمة خفيفة. كل عنصر يجب أن يكون مكوناً عرضياً بسيطاً مع أقل عدد من استدعاءات hooks. الحسابات المكلفة داخل عناصر القائمة تتضاعف عبر كل صف معروض.
- فكر في التقسيم إلى نوافذ أو الترقيم للقوائم التي لا تحتاج إلى إظهار كل العناصر في وقت واحد. أحياناً أفضل تحسين هو عدم عرض شيء على الإطلاق.
التحليل، تحليل الحزمة، وأنماط React 19+
أهم أداة أداء ليست مكتبة أو hook. إنها محلل React DevTools. قبل تحسين أي شيء، سجل جلسة تحليل أثناء إعادة إنتاج السلوك البطيء. المحلل يظهر لك بالضبط أي المكونات عُرضت، ولماذا عُرضت (تغيير خاصية، تغيير حالة، تغيير سياق، إعادة عرض أصل)، وكم استغرق كل عرض. بدون هذه البيانات، أنت تخمن.
تحليل الحزمة هو الأداة الأساسية الثانية. تطبيق React سريع يشحن 800 كيلوبايت من جافا سكريبت لا يزال بطيئاً لأن المتصفح يجب أن ينزلها ويحللها. أدوات مثل vite-bundle-visualizer أو statoscope أو source-map-explorer تولد تمثيلاً بصرياً لحزمتك، تظهر أي الحزم تساهم بأكبر عدد من البايتات. النتائج غالباً ما تكون مفاجئة — تبعية واحدة كبيرة (moment.js، مكتبة رسوم بيانية، مجموعة أيقونات) يمكن أن تشكل نصف حزمتك.
React 19 يقدم أنماطاً تغير مشهد تحسين الأداء. مكونات الخادم تعمل على الخادم وترسل فقط HTML المعروض إلى العميل، مما يلغي جافا سكريبت من جهة العميل للمكونات التي لا تحتاج إلى تفاعلية. هذا ليس تحسيناً متخصصاً — أي مكون يجلب البيانات ويعرضها دون حالة من جهة العميل أو معالجي أحداث يجب أن يكون مكون خادم. جافا سكريبت التي كانت سترسل إلى العميل أصبحت صفراً.
التمييز بين مكونات الخادم ومكونات العميل يخلق نموذجاً عقلياً جديداً للأداء. قبل اللجوء إلى memo، اسأل: هل يحتاج هذا المكون إلى العمل على العميل أصلاً؟ إذا كانت الإجابة لا، فقد ألغيت كل تكلفة إعادة عرض، وكل تكلفة ترطيب، وكل بايت من جافا سكريبت كان سيساهم به هذا المكون في الحزمة. مكونات الخادم ليست بديلاً عن أدوات تحسين عميل React. إنها استراتيجية أعلى مستوى تجعل تحسينات العميل غير ضرورية لجزء كبير من شجرة المكونات الخاصة بك.
تحسين الصور هو مجال آخر حيث يتولى الإطار ما اعتاد المطورون فعله يدوياً. مكونات Image في Next.js تخدم تلقائياً صوراً متجاوبة بالحجم والشكل الصحيحين، وتحميل الصور أسفل الجزء المرئي بطيئاً، وتمنع تغير التخطيط بحجز مساحة قبل تحميل الصورة. إذا كنت لا تزال تستخدم وسم img عادي مع JPEG كبير، هذا التغيير الواحد غالباً ما ينتج تحسيناً في الأداء أكبر من أي استراتيجية تخزين مؤقت.
- شغّل محلل React DevTools على أبطأ تفاعل في تطبيقك قبل إجراء أي تغييرات. احفظ التسجيل كمرجع أساسي.
- حلل حزمتك باستخدام vite-bundle-visualizer أو source-map-explorer. ابحث عن التبعيات الكبيرة التي يمكن استبدالها ببدائل أخف.
- حوّل مكونات جلب البيانات إلى مكونات خادم إذا كنت تستخدم React 19 مع إطار خادم. هذا يلغي تكلفتها بالكامل من جهة العميل.
- استبدل وسوم img بمكون Image الخاص بالإطار (next/image، إلخ) للحصول على صور متجاوبة تلقائية وتحميل بطيء.
- استخدم علامة التبويب الأداء في Chrome DevTools لقياس تأخير التفاعل، وتغيرات التخطيط، والمهام الطويلة — وليس فقط وقت عرض React.
وضع كل شيء معاً
تحسين أداء React يتبع تسلسلاً هرمياً واضحاً. في القمة هي العمارة: مكونات خادم تعمل على الخادم، تقسيم كود يبقي الحزم صغيرة، وتمرير افتراضي يحد من حجم DOM. في المنتصف هو التخزين المؤقت المستهدف: memo للمكونات الطرفية التي تعيد العرض دون داعٍ، useMemo للحسابات المكلفة، وuseCallback لمراجع الدوال المستقرة. في القاع — وهذا حيث يبدأ معظم الناس — هو التحسين الدقيق: الدوال المضمنة، مراجع الكائنات، واللف المبكر الذي يضيف تعقيداً دون فائدة قابلة للقياس.
اعمل من الأعلى إلى الأسفل. حلل أولاً، ثم تحقق من عمارتك، ثم طبق التخزين المؤقت المستهدف. لا تبدأ بالتحسينات الدقيقة. الفرق بين تطبيق React محسن جيداً وآخر سيئ التحسين ليس أبداً ما إذا كنت استخدمت memo في الأماكن الصحيحة. إنه ما إذا كانت عمارتك تسمح لـ React بالقيام بعمله بكفاءة — أشجار مكونات صغيرة، وحزم صغيرة، وأحجام DOM صغيرة، وتدفق بيانات واضح.
تقنيات هذا الدليل شاملة لكن المبدأ بسيط: اجعل سلوك React الافتراضي سريعاً بإعطائه عملاً أقل لفعله. اعرض مكونات أقل. اشحن جافا سكريبت أقل. احسب قيماً أقل. كلما طلبت من React أن يفعل أقل، كلما كان أسرع — وكلما قل تفكيرك في الأداء على الإطلاق.
