إرسال 500 كيلوبايت من جافاسكريبت غير ضروري للمتصفح ليس مجرد بطء، بل جريمة ضد تجربة المستخدم. اكتشف الأخطاء التقنية الشائعة في كتابة React وكيفية تجنبها بذكاء هندسي.
عندما تفتح أداة الـ Performance في متصفح كروم وترى أن تطبيقك يستغرق ١٢ ثانية ليظهر أول محتوى مفيد، بينما السيرفر يرد خلال ٢٠٠ مللي ثانية، فأنت أمام كارثة هندسية. المشكلة ليست في React نفسها، بل في الطريقة التي يستخدمها معظم المطورين لبناء التطبيقات. الحقيقة المؤلمة هي أن ٨٠٪ من مشاريع React التي أراجعها في الشركات تعاني من نفس الأخطاء التقنية: تحميل زائد لجافاسكريبت، إعادة رسم غير ضرورية، وحالة غير منضبطة تسرق موارد الجهاز. دعونا نفتح الصندوق الأسود ونرى ماذا يحدث خلف الكواليس.
لنبدأ بالأرقام الصادمة: متوسط حجم حزمة جافاسكريبت لتطبيق React حديث يتجاوز ٤٠٠ كيلوبايت بعد الضغط، وهذا يعني أن المستخدم على شبكة ٣G سيضطر لانتظار ٣-٥ ثوانٍ قبل أن يرى أي شيء على الشاشة. المشكلة الأكبر هي أن معظم هذا الكود لا يُستخدم في أول تحميل، بل يُحمل فقط لأن المطور قرر وضع كل شيء في ملف واحد. هذا ليس خطأ في React، بل في فهم خاطئ لكيفية عمل الـ Virtual DOM والـ Reconciliation Algorithm.
في مشروع سابق عملت عليه، كان فريق التطوير يستخدم React مع Redux ويحمّل كل الـ Reducers والـ Actions في ملف واحد بحجة "التنظيم". النتيجة؟ ملف جافاسكريبت بحجم ١.٢ ميجابايت يُحمل حتى قبل أن يرى المستخدم الصفحة الرئيسية. المشكلة هنا ليست في حجم الكود فقط، بل في أن المتصفح يضطر لتحليل وتنفيذ كل هذا الكود قبل أن يتمكن من رسم أي شيء على الشاشة. هذا ما يسمى بـ Blocking JavaScript، وهو أسوأ كابوس لتجربة المستخدم.
الحل ليس معقداً ولكنه يتطلب تغييراً في العقلية: يجب أن نفكر في التطبيق كسلسلة من الـ Micro Frontends، وليس ككتلة واحدة. استخدم Code Splitting مع React.lazy و Suspense لتقسيم التطبيق إلى أجزاء صغيرة تُحمل عند الحاجة فقط. لكن احذر: تقسيم الكود بشكل عشوائي قد يؤدي إلى مشكلة أخرى وهي الـ Waterfall Requests، حيث ينتظر المتصفح تحميل ملف ثم يكتشف أنه يحتاج لملف آخر، وهكذا. الحل الذكي هو استخدام Preloading للـ Routes التي من المحتمل أن ينتقل إليها المستخدم.
// مثال على Code Splitting مع Preloading الذكي
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
// تحميل الصفحة فقط عند الحاجة
const HomePage = lazy(() => import('./pages/Home'));
const DashboardPage = lazy(() => import('./pages/Dashboard'));
// Preload للصفحات المتوقعة
const preloadPages = () => {
import('./pages/Dashboard'); // سيبدأ التحميل في الخلفية
};
function App() {
return (
<Router>
<Suspense fallback={<div>جار التحميل...</div>}>
<Switch>
<Route exact path="/" comp{HomePage} />
<Route
path="/dashboard"
component={DashboardPage}
onMouseEnter={preloadPages} // Preload عند تمرير الماوس
/>
</Switch>
</Suspense>
</Router>
);
}في أحد المشاريع التي استشرتُ فيها، كان التطبيق يعيد رسم ٣٠٠ عنصر في القائمة عند تغيير حرف واحد في مربع البحث. المشكلة لم تكن في الخوارزمية نفسها، بل في أن المطور استخدم useEffect بدون تبعيات صحيحة، مما أدى إلى تشغيل الـ Effect في كل تغيير للحالة، حتى لو كان التغيير غير مرتبط بالبحث. هذا النوع من الأخطاء شائع جداً ويؤدي إلى ما يسمى بـ Unnecessary Re-renders، وهو أحد أكبر أسباب بطء تطبيقات React.
لفهم المشكلة بعمق، يجب أن نعرف كيف يعمل React تحت الغطاء. عندما يتغير الـ State أو الـ Props، يقوم React بتشغيل الـ Reconciliation Algorithm لمقارنة الـ Virtual DOM القديم بالجديد. إذا وجد اختلافات، يقوم بتحديث الـ Real DOM. لكن المشكلة أن هذا العملية مكلفة جداً، خاصة إذا كانت الشجرة كبيرة. لذلك، يجب علينا تقليل عدد المرات التي يحدث فيها هذا الـ Reconciliation، وهذا ما تفعله مكتبات مثل React.memo و useMemo و useCallback.
// مثال على استخدام React.memo لتقليل إعادة الرسم
import React, { useState, memo } from 'react';
// مكون يتم إعادة رسمه فقط عند تغيير الـ props
const ExpensiveComp memo(({ data }) => {
console.log('Rendering ExpensiveComponent'); // سيتم طباعته فقط عند تغيير data
return <div>{data}</div>;
});
function App() {
const [count, setCount] = useState(0);
const [data, setData] = useState('Hello');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<ExpensiveComponent data={data} />
{/* تغيير count لن يؤدي إلى إعادة رسم ExpensiveComponent */}
</div>
);
}
// مثال على استخدام useMemo لحفظ نتيجة حساب مكلف
function ComplexCalculation({ items }) {
const result = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0); // حساب مكلف
}, [items]); // يعاد الحساب فقط عند تغيير items
return <div>Result: {result}</div>;
}أحد أكثر الأخطاء شيوعاً هو استخدام useEffect بدون تبعيات، أو مع تبعيات غير صحيحة. مثلاً، كتابة useEffect بدون قائمة تبعيات يعني أنه سيعمل في كل مرة يتم فيها إعادة رسم المكون، وهذا كارثة للأداء. أسوأ من ذلك هو استخدام قائمة تبعيات غير كاملة، مما يؤدي إلى تشغيل الـ Effect في أوقات غير متوقعة. في مشروع سابق، وجدنا أن أحد المكونات كان يرسل طلب API في كل مرة يتغير أي جزء من الـ State، حتى لو كان التغيير غير مرتبط بالطلب!
الحل هو التفكير بعناية في قائمة التبعيات واستخدام أدوات مثل eslint-plugin-react-hooks للمساعدة في اكتشاف هذه الأخطاء. أيضاً، يجب تجنب وضع دوال داخل useEffect إذا كانت تعتمد على قيم خارجية، لأن ذلك قد يؤدي إلى استخدام قيم قديمة (Stale Closures). الحل هو استخدام useCallback للدوال التي نريد تمريرها كـ dependencies.
// مثال على استخدام useEffect بطريقة صحيحة
import React, { useState, useEffect, useCallback } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// استخدام useCallback لتجنب إعادة إنشاء الدالة في كل render
const fetchUser = useCallback(async () => {
const resp await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}, [userId]); // يعاد إنشاء الدالة فقط عند تغيير userId
useEffect(() => {
fetchUser();
}, [fetchUser]); // استخدام الدالة كdependency
return user ? <div>{user.name}</div> : <div>جار التحميل...</div>;
}
// مثال خاطئ شائع
function BadExample() {
const [count, setCount] = useState(0);
// هذا سيطبع العدد في كل render، وليس فقط عند تغيير count
useEffect(() => {
console.log('Count changed:', count);
}); // ❌ لا توجد قائمة تبعيات
return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
}في أحد المشاريع الكبيرة الذي عملت عليه، كان التطبيق يستهلك أكثر من ١ جيجابايت من الذاكرة بعد استخدامه لبضع دقائق فقط. المشكلة كانت في أن المطورين كانوا يستخدمون useState داخل مكونات يتم إعادة إنشاؤها باستمرار، مما يؤدي إلى فقدان الإشارة للحالة القديمة وعدم إمكانية جمعها بواسطة الـ Garbage Collector. هذا النوع من الـ Memory Leaks صعب الكشف عنه لأن التطبيق يبدو وكأنه يعمل بشكل طبيعي، ولكنه يبطئ الجهاز تدريجياً حتى يتجمد تماماً.
السبب الرئيسي لهذه المشكلة هو عدم فهم دورة حياة المكونات في React. عندما يتم إلغاء تحميل مكون، يجب علينا تنظيف أي موارد يستخدمها، مثل الاشتراكات في الأحداث أو الـ Timers أو الـ WebSocket Connections. إذا لم نقم بذلك، ستظل هذه الموارد موجودة في الذاكرة، مما يؤدي إلى تسرب الذاكرة. أيضاً، استخدام useState داخل مكونات يتم إعادة إنشاؤها باستمرار (مثل داخل قائمة) يؤدي إلى إنشاء حالة جديدة في كل مرة، مما يزيد من استهلاك الذاكرة.
// مثال على تنظيف الموارد في useEffect
import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// تنظيف الـ interval عند إلغاء تحميل المكون
return () => clearInterval(interval);
}, []); // قائمة تبعيات فارغة = يعمل مرة واحدة عند التحميل
return <div>Seconds: {seconds}</div>;
}
// مثال على تجنب Memory Leak في مكونات القائمة
function UserList({ users }) {
return (
<ul>
{users.map(user => (
// استخدام key فريد لمنع إعادة إنشاء المكونات
<UserItem key={user.id} user={user} />
))}
</ul>
);
}
function UserItem({ user }) {
// هذا المكون سيتم إعادة إنشاؤه فقط عند تغيير الـ key
// وليس عند إعادة رسم القائمة، مما يمنع Memory Leak
return <li>{user.name}</li>;
}في أحد المشاريع التي راجعت كودها، كان التطبيق يتجمد تماماً عند تحميل ملف كبير. المشكلة لم تكن في حجم الملف نفسه، بل في أن المطور استخدم دالة sync لحساب الهاش للملف داخل الـ Main Thread. هذا أدى إلى توقف كل شيء في المتصفح، بما في ذلك الرسوم المتحركة والتفاعل مع المستخدم، لمدة ٣ ثوانٍ كاملة. هذا النوع من الأخطاء شائع جداً ويحدث عندما لا نفهم كيف يعمل الـ Event Loop في المتصفح.
المتصفح لديه thread واحد فقط لتنفيذ جافاسكريبت ورسم واجهة المستخدم. إذا قمنا بتشغيل عملية مكلفة داخل هذا الـ Thread، مثل معالجة ملف كبير أو تنفيذ خوارزمية معقدة، فإن كل شيء آخر سيتوقف حتى تنتهي العملية. الحل هو استخدام الـ Web Workers لتشغيل العمليات المكلفة في الخلفية، أو تقسيم العمل إلى أجزاء صغيرة باستخدام setTimeout أو requestIdleCallback. أيضاً، يجب تجنب استخدام دوال sync مثل JSON.parse أو localStorage.getItem داخل الـ Main Thread إذا كانت البيانات كبيرة.
// مثال على استخدام Web Worker لتجنب Blocking Main Thread
// worker.js
self. function(e) {
const data = e.data;
// عملية مكلفة في الخلفية
const result = heavyComputation(data);
self.postMessage(result);
};
function heavyComputation(data) {
// محاكاة عملية مكلفة
let result = 0;
for (let i = 0; i < 1e9; i++) {
result += Math.sqrt(i);
}
return result;
}
// Main Thread
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
console.log('Result from worker:', e.data);
};
worker.postMessage({ /* data */ });
// مثال على تقسيم العمل باستخدام setTimeout
function processLargeArray(array, callback) {
const chunkSize = 1000;
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (; index < end; index++) {
// معالجة العنصر
}
if (index < array.length) {
setTimeout(processChunk, 0); // السماح للـ Event Loop بمعالجة الأحداث الأخرى
} else {
callback();
}
}
processChunk();
}بعد كل هذه الأخطاء، قد تتساءل: كيف نكتب React بطريقة صحيحة؟ الحل ليس في استخدام مكتبة معينة أو اتباع نمط محدد، بل في فهم عميق لكيفية عمل المتصفح وReact تحت الغطاء. يجب أن نفكر في تطبيقنا كمجموعة من المكونات المستقلة التي تتواصل مع بعضها بطريقة فعالة، وليس ككتلة واحدة ضخمة. إليك بعض المبادئ التي يجب اتباعها:
في النهاية، كتابة React بطريقة صحيحة ليست مسألة تقنية فقط، بل هي مسألة هندسية. يجب أن نفكر في المستخدم النهائي وكيفية توفير تجربة سلسة وسريعة، وليس فقط في كتابة الكود الذي "يعمل". تذكر أن ٥٠٪ من المستخدمين سيغادرون الموقع إذا استغرق التحميل أكثر من ٣ ثوانٍ، وهذا يعني أن كل كيلوبايت وكل مللي ثانية مهمة. لذا، في المرة القادمة التي تكتب فيها مكون React، اسأل نفسك: هل هذا الكود ضروري الآن؟ هل هناك طريقة لجعله أسرع؟ هل يمكنني تقسيمه؟ الإجابة على هذه الأسئلة هي ما يميز المطور الجيد عن المطور العادي.
إذا أردت تلخيص كل ما قلناه في سطرين، فإليك النصيحة الذهبية: "لا تكتب مكون React قبل أن تفهم كيف يعمل الـ Event Loop في المتصفح وكيف يتعامل React مع الـ Virtual DOM. كل سطر كود تكتبه يجب أن يكون له هدف واضح: إما تحسين تجربة المستخدم، أو تقليل استهلاك الموارد، أو تسهيل الصيانة. إذا لم يكن الكود يفعل واحداً من هذه الأشياء، احذفه."