إرسال 5 ميجابايت من جافاسكريبت للمتصفح ليس مجرد بطء، بل هو جريمة ضد تجربة المستخدم. اكتشف الأخطاء الشائعة في كتابة React وكيف تدمر الأداء، وكيفية إصلاحها بتقنيات حقيقية مستخدمة في الشركات الكبرى.
عندما تفتح أداة الـ Performance في كروم وترى أن تطبيقك يستغرق ١٢ ثانية ليظهر زراً واحداً، تدرك أن المشكلة ليست في سرعة الإنترنت. المشكلة في الطريقة التي تكتب بها React. معظم المطورين يكتبون مكوناتهم وكأنهم ما زالوا في عام ٢٠١٦، عندما كانت الـ Hooks مجرد تجربة ولم يكن هناك مفهوم الـ Concurrent Mode أو الـ Server Components. اليوم، كل سطر زائد في الكود يعني وقت تحميل أطول، وكل useEffect غير مضبوط يعني إعادة رسم لا داعي لها. الحقيقة هي: معظم تطبيقات React التي تراها في الإنتاج مكتوبة بطريقة خاطئة، حتى لو كانت تعمل.
الخطأ ليس في React نفسها، بل في كيفية استخدامها. المطورون يفرطون في استخدام useState داخل loops، ينسون أن كل setState يسبب إعادة رسم كامل للشجرة، ويكتبون useEffect بدون cleanup كما لو كانوا يكتبون jQuery في عام ٢٠١٠. النتيجة؟ تطبيقات بطيئة، ذاكرة تتسرب، ومستخدمون يغلقون الصفحة بعد ثانيتين. في هذا المقال، سنفكك الأخطاء التقنية التي يرتكبها حتى المطورون ذوو الخبرة، ونعرض الحلول العملية التي تستخدمها فرق مثل فريق فيسبوك وميتا لتحسين أداء تطبيقاتهم.
تخيل أنك تريد عرض قائمة من ١٠٠٠ عنصر، وكل عنصر يحتاج إلى حالة مستقلة. الحل الساذج هو كتابة map داخل المكون واستخدام useState لكل عنصر. المشكلة هنا أن React ستعيد رسم المكون بالكامل في كل مرة يتغير فيها أي state، حتى لو كان التغيير في عنصر واحد فقط. هذا ليس مجرد مشكلة أداء، بل هو خطأ في فهم كيفية عمل React تحت الغطاء.
عندما تستخدم useState داخل loop، فأنت تخبر React أن هذا المكون يعتمد على ١٠٠٠ حالة مختلفة. كل مرة يتغير أي منها، سيقوم React بإعادة حساب الـ Virtual DOM بالكامل، مقارنة كل شيء، ثم إعادة رسم ما تغير. هذا السيناريو يتكرر في تطبيقات التجارة الإلكترونية عندما تعرض قائمة منتجات مع أزرار «أضف إلى السلة» لكل منتج. الحل؟ استخدام مفتاح فريد لكل عنصر واستخدام useReducer بدلاً من useState، أو الأفضل، استخدام مكتبة مثل zustand لإدارة الحالة خارج المكونات تماماً.
// ❌ خطأ شائع: استخدام useState داخل loop
const ProductList = ({ products }) => {
return products.map(product => {
const [quantity, setQuantity] = useState(1);
return (
<div key={product.id}>
<button {() => setQuantity(q => q + 1)}>+</button>
{quantity}
</div>
);
});
};
// ✅ الحل الصحيح: إدارة الحالة خارج المكون
const useCartStore = create(set => ({
items: {},
addItem: (id) => set(state => ({
items: { ...state.items, [id]: (state.items[id] || 0) + 1 }
}))
}));
const ProductList = ({ products }) => {
const { items, addItem } = useCartStore();
return products.map(product => (
<div key={product.id}>
<button onClick={() => addItem(product.id)}>+</button>
{items[product.id] || 0}
</div>
));
};في المثال الخاطئ، كل مرة تضغط على زر «+»، React ستعيد رسم المكون الأب بالكامل لأن حالة كل عنصر تعتبر جزءاً من حالة المكون الأب. هذا يعني أنه حتى لو كان لديك ١٠٠٠ منتج، سيقوم React بإعادة حساب الـ Virtual DOM لـ ١٠٠٠ عنصر، مقارنة كل شيء، ثم إعادة رسم العنصر الذي تغير فقط. في المثال الصحيح، الحالة مخزنة خارج المكون باستخدام zustand، مما يعني أن React لا تحتاج لإعادة رسم أي شيء إلا العنصر الذي تغير فقط. هذا الفرق بين O(n) و O(1) في إعادة الرسم.
useEffect هي واحدة من أكثر الـ Hooks سوء استخدام في React. المطورون يكتبونها وكأنها نسخة حديثة من setTimeout أو setInterval، وينسون أن كل useEffect بدون cleanup هو تسرب ذاكرة محتمل. تخيل أنك تفتح صفحة في تطبيقك وتبدأ استماعاً لـ WebSocket أو تستدعي API كل ثانية. إذا لم تقم بعمل cleanup عند مغادرة الصفحة، ستستمر هذه العمليات في الخلفية، مما يؤدي إلى استهلاك ذاكرة ومعالج بشكل غير ضروري.
المشكلة الأكبر هي أن معظم المطورين لا يدركون أن cleanup ليس مجرد «تنظيف»، بل هو جزء أساسي من دورة حياة المكون. عندما تغادر صفحة تحتوي على useEffect بدون cleanup، React لن تقوم بإلغاء الاشتراكات أو إيقاف الـ Timers تلقائياً. هذا السلوك مشابه لتسرب الذاكرة في تطبيقات C++، حيث الكائنات تبقى في الذاكرة لأنك نسيت تحريرها. في تطبيقات React الكبيرة، هذا يؤدي إلى تباطؤ تدريجي في الأداء، خاصة في تطبيقات الـ Single Page Applications التي تبقى مفتوحة لساعات.
// ❌ خطأ شائع: useEffect بدون cleanup
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/data').then(res => res.json()).then(setData);
}, 1000);
// لا cleanup!
}, []);
// ✅ الحل الصحيح: cleanup لكل effect
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/data').then(res => res.json()).then(setData);
}, 1000);
return () => clearInterval(interval); // cleanup
}, []);
// مثال متقدم: cleanup للـ WebSocket
useEffect(() => {
const socket = new WebSocket('wss://example.com');
socket. (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => socket.close(); // cleanup
}, []);يمكنك استخدام أدوات الـ Performance و Memory في كروم لقياس تأثير الـ useEffect بدون cleanup. افتح أداة الـ Memory، خذ لقطة قبل وبعد دخول الصفحة، ثم مغادرتها. إذا رأيت أن عدد الـ Event Listeners أو الـ Timers لم ينخفض، فهذا يعني أن هناك تسرب ذاكرة. في تطبيقات الإنتاج، هذا يؤدي إلى زيادة استخدام المعالج والبطارية، خاصة على الأجهزة المحمولة. الحل؟ دائماً اكتب cleanup لكل useEffect، حتى لو كنت تعتقد أنه ليس ضرورياً. القاعدة الذهبية: إذا قمت بإنشاء شيء ما في useEffect، يجب أن تقوم بإزالته في cleanup.
معظم المطورين يكتبون React وكأنها مكتبة تعمل بطريقة متزامنة بالكامل، بينما الحقيقة هي أن React منذ الإصدار ١٨ تعمل بطريقة غير متزامنة باستخدام الـ Concurrent Mode. هذا يعني أن React يمكنها إيقاف وإعادة تشغيل عملية الرسم بناءً على أولوية المهام. المشكلة هي أن الكثير من الكود المكتوب اليوم لا يأخذ هذا السلوك في الاعتبار، مما يؤدي إلى مشاكل في الأداء وتجربة مستخدم غير سلسة.
على سبيل المثال، عندما تستخدم useState داخل مكون، React قد تقرر إعادة رسم المكون عدة مرات قبل أن تظهر النتيجة للمستخدم. إذا كان لديك حسابات ثقيلة داخل المكون، مثل معالجة بيانات كبيرة أو حسابات رياضية معقدة، فإن هذا سيؤدي إلى تجميد واجهة المستخدم. الحل؟ استخدام useMemo و useCallback لتقليل كمية العمل التي تقوم بها React في كل إعادة رسم، واستخدام الـ Suspense للتعامل مع البيانات غير المتزامنة بطريقة أكثر كفاءة.
// ❌ خطأ شائع: حسابات ثقيلة بدون memoization
const ExpensiveComp ({ data }) => {
const processedData = data.map(item => {
// عملية حسابية ثقيلة
return heavyCalculation(item);
});
return <div>{processedData.join(', ')}</div>;
};
// ✅ الحل الصحيح: استخدام useMemo
const ExpensiveComponent = ({ data }) => {
const processedData = useMemo(() => {
return data.map(item => heavyCalculation(item));
}, [data]); // إعادة الحساب فقط عند تغير data
return <div>{processedData.join(', ')}</div>;
};
// مثال متقدم: استخدام React.lazy مع Suspense
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
};في التطبيقات التقليدية، عندما تقوم بإجراء تغيير على الحالة، React ستقوم بإعادة رسم المكون بالكامل قبل أن تستجيب لأي تفاعل آخر من المستخدم. هذا يعني أنه إذا كان لديك مكون ثقيل، فإن واجهة المستخدم ستتجمد حتى تنتهي عملية الرسم. مع الـ Concurrent Mode، React يمكنها إيقاف عملية الرسم مؤقتاً إذا اكتشف أن المستخدم يحاول التفاعل مع الصفحة، ثم تستأنف الرسم لاحقاً. هذا السلوك يجعل التطبيق يبدو أكثر استجابة، حتى لو كان هناك الكثير من العمل يجري خلف الكواليس.
لا يزال الكثير من المطورين يكتبون مكونات React بحجم ملفات PHP القديمة، حيث يحتوي المكون الواحد على مئات الأسطر من المنطق، الحالة، و JSX. هذا ليس مجرد مشكلة تنظيمية، بل هو خطأ في فهم كيفية عمل React. عندما يكون لديك مكون ضخم، فإن أي تغيير صغير في الحالة سيؤدي إلى إعادة رسم المكون بالكامل، مما يؤدي إلى تباطؤ الأداء وتجربة مستخدم سيئة.
المشكلة الأكبر هي أن المكونات الضخمة تجعل من الصعب تتبع تدفق البيانات وفهم أين تحدث الأخطاء. في تطبيقات الإنتاج، هذا يؤدي إلى زيادة وقت التصحيح وتكاليف الصيانة. الحل؟ تقسيم المكونات إلى مكونات أصغر وأكثر تخصصاً، واستخدام نمط الـ Compound Components أو الـ Render Props لإعادة استخدام المنطق بين المكونات. على سبيل المثال، بدلاً من كتابة مكون Dashboard يحتوي على كل شيء، قم بتقسيمه إلى مكونات أصغر مثل Sidebar، Header، و MainContent.
// ❌ خطأ شائع: مكون ضخم
const Dashboard = () => {
const [user, setUser] = useState(null);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
fetch('/api/data').then(res => res.json()).then(setData);
setLoading(false);
}, []);
return (
<div>
<header>...</header>
<sidebar>...</sidebar>
<main>
{loading ? <Spinner /> : <DataTable data={data} />}
</main>
</div>
);
};
// ✅ الحل الصحيح: تقسيم المكونات
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
const DataFetcher = ({ children }) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data').then(res => res.json()).then(data => {
setData(data);
setLoading(false);
});
}, []);
return children({ data, loading });
};
const Dashboard = () => {
return (
<UserProvider>
<DataFetcher>
{({ data, loading }) => (
<div>
<Header />
<Sidebar />
<main>
{loading ? <Spinner /> : <DataTable data={data} />}
</main>
</div>
)}
</DataFetcher>
</UserProvider>
);
};عندما يكون لديك مكون ضخم، React ستحتاج إلى إعادة حساب الـ Virtual DOM بالكامل في كل مرة يتغير أي جزء من الحالة. هذا يعني أنه حتى لو كان التغيير بسيطاً، مثل تغيير لون زر، فإن React ستعيد حساب كل شيء داخل المكون. في المقابل، عندما يكون لديك مكونات صغيرة ومتخصصة، فإن React ستعيد رسم المكونات المتأثرة فقط. هذا الفرق يمكن أن يكون هائلاً في التطبيقات الكبيرة، حيث قد يقلل وقت الرسم من مئات المللي ثانية إلى بضعة مللي ثانية فقط.
مع ظهور React Server Components في الإصدار ١٨، أصبح بإمكان المطورين كتابة مكونات تعمل على السيرفر بدلاً من المتصفح. هذه الميزة ليست مجرد تحسين للأداء، بل هي تغيير جذري في كيفية كتابة تطبيقات React. المشكلة هي أن معظم المطورين ما زالوا يكتبون كل مكوناتهم للعمل على المتصفح، مما يؤدي إلى إرسال كميات هائلة من جافاسكريبت غير الضرورية للمستخدم.
على سبيل المثال، إذا كان لديك مكون يعرض بيانات من قاعدة بيانات، فلماذا ترسل الكود الخاص بهذا المكون إلى المتصفح؟ بدلاً من ذلك، يمكنك كتابة هذا المكون ليعمل على السيرفر، حيث يمكنه جلب البيانات وإرسال HTML جاهز للمتصفح. هذا يقلل من كمية الجافاسكريبت المرسلة، ويحسن وقت التحميل، ويقلل من استخدام المعالج على الأجهزة المحمولة. في تطبيقات الإنتاج، هذا يمكن أن يقلل وقت التحميل من عدة ثوانٍ إلى أقل من ثانية واحدة.
// ❌ خطأ شائع: جلب البيانات في المتصفح
'use client';
export default function ProductPage({ id }) {
const [product, setProduct] = useState(null);
useEffect(() => {
fetch(`/api/products/${id}`).then(res => res.json()).then(setProduct);
}, [id]);
if (!product) return <div>Loading...</div>;
return <div>{product.name}</div>;
}
// ✅ الحل الصحيح: استخدام Server Component
// ملف: app/products/[id]/page.js
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <div>{product.name}</div>;
}عندما تستخدم Server Component، React ستقوم بتشغيل هذا المكون على السيرفر أثناء عملية البناء أو عند الطلب، اعتماداً على الإعدادات. المكون سيقوم بجلب البيانات، تنفيذ المنطق، ثم إرسال HTML جاهز للمتصفح. هذا يعني أن المتصفح لا يحتاج إلى تنفيذ أي جافاسكريبت لهذا المكون، مما يقلل من وقت التحميل واستخدام المعالج. بالإضافة إلى ذلك، لأن البيانات تأتي مباشرة من السيرفر، فإنك تقلل من عدد الطلبات التي يحتاج المتصفح إلى إرسالها، مما يحسن الأداء بشكل كبير.
الكتابة الصحيحة لـ React ليست مجرد مسألة أسلوب، بل هي مسألة فهم كيف تعمل المكتبة تحت الغطاء وكيف تؤثر قراراتك على تجربة المستخدم النهائي. القاعدة الأولى: لا تستخدم useState داخل loops، واستخدم مكتبات إدارة الحالة مثل zustand أو redux بدلاً من ذلك. القاعدة الثانية: دائماً اكتب cleanup لكل useEffect، حتى لو كنت تعتقد أنه ليس ضرورياً. القاعدة الثالثة: استفد من الـ Concurrent Mode باستخدام useMemo و useCallback لتقليل كمية العمل التي تقوم بها React في كل إعادة رسم. القاعدة الرابعة: قسم مكوناتك إلى مكونات أصغر وأكثر تخصصاً، واستخدم أنماط مثل Compound Components لإعادة استخدام المنطق. القاعدة الخامسة: استفد من Server Components لتقليل كمية الجافاسكريبت المرسلة للمتصفح.
في النهاية، كتابة React بالطريقة الصحيحة تعني كتابة تطبيقات أسرع، أكثر استجابة، وأسهل في الصيانة. لا تكتب React كما لو كنت في عام ٢٠١٦، اكتبها كما لو كنت في عام ٢٠٢٤، حيث المستخدمون يتوقعون تطبيقات تعمل بسرعة البرق حتى على أضعف الأجهزة.