5322 words
27 minutes
JavaScript Preformance Optimization

مقدمة في الـ Performance#

ليه الـ Performance مهم في مواقع الويب؟#

  1. تجربة اليوزر 🚀

    • معظم الناس مش بتستحمل تستنى الموقع يحمل أكتر من 3 ثواني
    • نص اليوزرز تقريباً بيقفلوا المواقع البطيئة على طول
  2. التأثير على السيرش إنجن (SEO) 🔍

    • جوجل بيهتم جداً بسرعة المواقع
    • المواقع السريعة بتطلع في الأول في نتايج البحث
    • الـ Core Web Vitals دلوقتي بقت مهمة جداً
  3. توفير في المصاريف 💰

    • الموقع السريع مش بيتحمل ضغط على السيرفرات، وبالتالي فاتورة الهوستنج بتقل.
    • استهلاك الموارد بيكون أقل، وده بيوفرلك في مصاريف البنية التحتية.
  4. زيادة المبيعات (Conversion Rate) 📈

    • موقع سريع يعني فرص بيع أكبر، لأن الزباين هيفضلوا على الموقع ومش هيسيبوه بسرعة.
  5. سهولة الوصول للموقع (Accessibility) ♿

    • حتى الناس اللي النت عندهم ضعيف هيقدروا يستخدموا الموقع
    • أصحاب الهمم هيقدروا يستخدموا الموقع بسهولة

الـ performance في الجافاسكريبت بيتقاس من خلال:#

  • 🔄 سرعة تحميل السكريبتات
  • ⚡ سرعة تنفيذ الكود
  • 💾 حجم الذاكرة المستخدمة
  • 🌐 كفاءة التنفيذ في المتصفح

مقاييس الأداء المهمة في المواقع 🎯#

1. سرعة التحميل (الموقع بيفتح إزاي) 🚀#

First Contentful Paint (FCP)

  • دي أول حاجة المستخدم بيشوفها لما يفتح موقعك
  • يعني بعد كام ثانية هيظهر أول عنصر في الصفحة (نص، صورة، أي حاجة)
  • المفروض تكون أقل من 1.8 ثانية
  • لو زادت عن كده يبقى في مشكلة محتاجة تتظبط

Largest Contentful Paint (LCP)

  • دي أكبر حاجة ظاهرة في الصفحة (زي البانر الكبير أو الصورة الرئيسية)
  • المفروض تظهر في أقل من 2.5 ثانية
  • لو بتاخد وقت كبير، يبقى لازم نشوف:
    • حجم الصور كبير؟
    • السيرفر بطيء؟
    • الكود محتاج تحسين؟

Time to Interactive (TTI)

  • يعني إمتى الصفحة هتبقى جاهزة للاستخدام بالكامل
  • مش بس تظهر، لأ وكمان تقدر تضغط على الأزرار وتتفاعل معاها
  • المفروض متزيدش عن 3.8 ثانية
  • لو زايدة، يبقى JavaScript بتاعك محتاج مراجعة

2. كفاءة التشغيل (الموقع شغال كويس إزاي)#

First Input Delay (FID)

  • الوقت اللي بياخده الموقع عشان يستجيب لأول ضغطة من اليوزر
  • يعني لما تدوس على زرار، هيستجيب بعد كام مللي ثانية
  • لازم يكون أقل من 100 مللي ثانية
  • لو في تأخير، غالباً في مشكلة في الـ JavaScript بتاعك

Total Blocking Time (TBT)

  • مجموع الوقت اللي الصفحة بتبقى فيه “مشغولة”
  • يعني الوقت اللي مش بتقدر تضغط فيه على حاجة
  • لازم تكون أقل ما يمكن
  • بتزيد لما يكون في:
    • كود كتير بيتنفذ مرة واحدة
    • عمليات معقدة في الخلفية

Cumulative Layout Shift (CLS)

  • دي بتقيس الحركة المفاجئة للعناصر في صفحتك
  • زي لما تكون هتدوس على زرار وفجأة يتحرك من مكانه
  • المفروض تكون أقل من 0.1
  • بتحصل غالباً بسبب:
    • صور من غير أبعاد محددة
    • إعلانات بتظهر فجأة
    • محتوى بيتحمل بعد تحميل الصفحة

المشاكل الشائعة في الـ DOM:#

  1. كثرة التحديثات المباشرة

لما نغير في الـ DOM على طول كده، بنخلي البراوزر يشتغل كتير على الفاضي

// مثال للكود الوحش ❌
for (let i = 0; i < 100; i++) {
    element.style.left = i + 'px';  // كل مرة بنغير حاجة صغيرة
}

// الطريقة الصح ✅
requestAnimationFrame(() => {
    element.style.left = '100px';  // نغير مرة واحدة وخلاص
});
  1. قراءة وكتابة متكررة

كل شوية نقرا حاجة ونكتب حاجة، ده بيبطئ الموقع جامد

// طريقة مش كويسة ❌
const height = element.offsetHeight;  // قراية
element.style.height = height + 10 + 'px';  // كتابة
const newHeight = element.offsetHeight;  // قراية تاني
// وهكذا...

// الطريقة الصح ✅
const height = element.offsetHeight;  // نقرا مرة واحدة
// نعمل كل التغييرات مع بعض
element.style.height = height + 10 + 'px';
element.style.width = '100px';
element.style.margin = '10px';
  1. Reflow & Repaint المتكرر
  • الـ Reflow: لما البراوزر يعيد حساب مكان وحجم كل العناصر
  • الـ Repaint: لما يعيد رسم العناصر من جديد

الاتنين دول بياخدوا وقت كتييير

// مثال وحش ❌
element.style.width = '100px';  // reflow
element.style.height = '100px'; // reflow تاني
element.style.margin = '10px';  // reflow تالت

// الطريقة الصح ✅
element.classList.add('new-style');  // مرة واحدة بس

الحلول التفصيلية:#

1. استخدام DocumentFragment#

أولاً، يعني إيه DocumentFragment؟ 🤔#

  • ده زي وعاء مؤقت أو “باكيت” بنحط فيه العناصر قبل ما نحطها في الصفحة
  • مبيظهرش في الصفحة نفسها
  • خفيف على الميموري والأداء
  • بيخلينا نعمل كل التغييرات في مكان واحد بعيد عن الـDOM

الطريقة الوحشة وليه هي وحشة ❌ ?#

  • كل لفة بتغير في الـDOM
  • كل مرة البراوزر بيعيد قراية وكتابة الـHTML
  • بتعمل parsing كتير
  • بتاخد ميموري أكتر
  • ممكن تعمل memory leaks
function addItems(items) {
    // ✅ الطريقة المثالية
    const fragment = document.createDocumentFragment();
    items.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item;
        fragment.appendChild(li);
    });
    list.appendChild(fragment);

    // ❌ الطريقة السيئة
    // items.forEach(item => {
    //     list.innerHTML += `<li>${item}</li>`;
    // });
}

2. ازاي نجمع التحديثات على الـDOM بالطريقة الصح#

المشكلة إيه؟ 🤔

  • لما نغير أي حاجة في الـDOM، البراوزر بيعمل عملية تسمى “Reflow”
  • الـReflow ده بيعني إن البراوزر بيعيد حساب مكان وحجم كل العناصر في الصفحة
  • ده بيغلبنا في تطبيقات زي الألعاب أو الأنيميشن

ليه الطريقة التانية وحشة ❌#

  • كل تغيير بيعمل Reflow
  • تغييرات متشتتة بتخلي الأنيميشن تتقطع
  • البراوزر بيشتغل أكتر
  • ممكن تخلي الموقع يهنج
function updateElements() {
    // ✅ الطريقة الكويسة
    requestAnimationFrame(() => {
        // هنا بنجمع كل التغييرات في كتلة واحدة
        element1.style.transform = 'translateX(100px)'; 
        element2.style.opacity = '0.5';
        element3.style.backgroundColor = 'red';
    });

}
 // ❌ الطريقة السيئة
// element1.style.transform = 'translateX(100px)';
// element2.style.opacity = '0.5'; 
// element3.style.backgroundColor = 'red';

نفس الـconcept موجود في ريأكت تحت مسمى الـBatching وإتكلمت عنه بالتفصيل هنا#

3. أهمية تخزين مراجع الـDOM#

المشكلة إيه؟ 🤔

  • لما بنستخدم الـDOM بكثرة، بيبقى في مشكلة في الأداء
  • دي بتحصل لما بنحاول نوصل لنفس العناصر عن طريق البحث في الـDOM كل مرة
  • ده بيخلي البراوزر يشتغل كتير في البحث والوصول للعناصر

ليه الطريقة التانية وحشة ❌

  • الـReflow بيكون أعلى
  • الميموري بيتستهلك أكتر
// ✅ الطريقة المثالية
const menu = document.querySelector('.menu');
const menuItems = menu.querySelectorAll('.menu-item');

menuItems.forEach(item => {
    // العمل مع العناصر
});

// ❌ الطريقة السيئة
// document.querySelectorAll('.menu .menu-item').forEach(...)

4.الطرق اللاتزامنية وازاي تستخدمها#

1. Promises 🤝#

  • الـPromises بتساعدنا في التعامل مع العمليات اللاتزامنية زي الاتصالات بالسيرفر
  • بتحل مشكلة “callback hell” اللي كانت بتحصل في الطرق القديمة
// ✅ استخدام Promise.all للتوازي
async function fetchAllData() {
    try {
        // بنعمل مصفوفة من الـPromises عشان نتعامل معاها بالتوازي
        const promises = urls.map(url => fetch(url));
        // بننتظر إنهاء كل الـPromises دول
        const responses = await Promise.all(promises);
        // بنحول النتايج لصيغة جيسون
        return await Promise.all(responses.map(r => r.json()));
    } catch (error) {
        console.error('Error fetching data:', error);
        // بنرفع الخطأ عشان نتعامل معاه في مكان تاني
        throw error;
    }
}

// ✅ استخدام Promise.race للتايم اوت
function fetchWithTimeout(url, timeout = 5000) {
    const controller = new AbortController();
    // بنعمل Promise للتايم اوت
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => {
            controller.abort();
            reject(new Error('Timeout'));
        }, timeout);
    });

    // بنستخدم Promise.race عشان نطلع من الاتنين اللي يخلص الأول
    return Promise.race([
        fetch(url, { signal: controller.signal }),
        timeoutPromise
    ]);
}

2. Async/Await 🤖#

  • الـAsync/Await بتخلينا نكتب كود اللاتزامني بطريقة أقرب للتزامني
  • بتساعدنا في معالجة الأخطاء والتنظيف بطريقة أفضل
// ✅ معالجة الأخطاء بشكل صحيح
async function processData() {
    try {
        // بننتظر حتى إنهاء الـPromises
        const data = await fetchData();
        const processedData = await processStep1(data);
        return await processStep2(processedData);
    } catch (error) {
        // بنعالج الخطأ بطريقة مناسبة
        logError(error);
        showUserFriendlyError();
        // بنعيد رفع الخطأ عشان نتعامل معاه في مكان تاني
        throw error;
    } finally {
        // بننظف الموارد اللي استخدمناها
        cleanup();
    }
}

ليه الطرق اللاتزامنية مهمة؟ 🤔#

  • حل مشكلة الاستجابة البطيئة

  • بتخلي التطبيق ميقفش لما بتنفذ عمليات اللاتزامنية المستخدم بيستمر في التفاعل مع التطبيق

  • إمكانية التوازي بنقدر نشغل عمليات مختلفة في نفس الوقت وده بيخلي التطبيق أسرع

  • الـTry/Catch بتساعدنا نتعامل مع الأخطاء بشكل أفضل والـFinally بتساعدنا في التنظيف

5. ازاي نحسن أداء الـLoops#

1. تحسينات أساسية:#

في كتير من الأحيان بنستخدم الـLoop بشكل روتيني عشان نلف على مصفوفة ونعمل عليها حاجة. بس في طرق أحسن من كده:

// ✅ استخدام طرق المصفوفة المدمجة
const numbers = [1, 2, 3, 4, 5];

// التصفية والتحويل في خطوة واحدة
const processedNumbers = numbers
    .filter(n => n > 2)
    .map(n => n * 2);

// ❌ تجنب
// const processedNumbers = [];
// for (let i = 0; i < numbers.length; i++) {
//     if (numbers[i] > 2) {
//         processedNumbers.push(numbers[i] * 2);
//     }
// }

2. تحسينات متقدمة:#

الطرق دي مش مخصوصة بالـLoops بس بتساعد جداً في تحسين أدائها:

// ✅ استخدام Set للبحث السريع
const uniqueItems = new Set(array);
const hasItem = uniqueItems.has(searchItem); // O(1)

// ✅ استخدام break عند العثور على النتيجة
for (const item of items) {
    if (matchCondition(item)) {
        result = item;
        break; // الخروج فور إيجاد النتيجة
    }
}
// ده بيخلي اللوب يخلص لما نلاقي النتيجة اللي بنبحث عنها

// ✅ تجنب إنشاء مصفوفات جديدة عند عدم الحاجة
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
// بدلاً من
// const sum = numbers.map(n => n).reduce((acc, curr) => acc + curr, 0);
// اللي بيعمل مصفوفة جديدة ومش محتاجها

6. إدارة الأحداث (events)#

بنواجه كتير مواقف بتتطلب منا التحكم في كمية الأحداث اللي بتوصل للكود بتاعنا. ده عشان نقلل الأحمال على السيرفر وفي نفس الوقت نحافظ على أداء تطبيقاتنا

1. Debouncing (التأخير):#

الفكرة الأساسية هي تأخير تنفيذ فانكشن علي أما تنتهي من الحدث اللي بتتكرره.
دي بتبقى مفيدة في حالات زي البحث مثلاً لما الواحد بيكتب في حقل البحث، مش عاوزين نبعت طلبات للسيرفر كل ما الواحد يكتب حرف واحد لأن ده هيكلف السيرفر وهيأثر على أداء التطبيق.

// بنعمل فانكشن اسمها debounce عشان نمنع استدعاء الفانكشن بشكل متكرر
// الفانكشن دي بتاخد اتنين باراميتر:
// func: ودي الفانكشن اللي عايزين ننفذها بعد فترة انتظار
// wait: ودي مدة الانتظار بالمللي ثانية
function debounce(func, wait) {
  let timeout; // هنا بنعرف متغير اسمه timeout عشان نستخدمه لتخزين الـ timeout ID

  // بنرجع فانكشن جديدة اللي هي اللي هتتنفذ بدل الفانكشن الأصلية
  return function executedFunction(...args) {
    // بنعرف فانكشن داخلية اسمها later بتنفذ الفانكشن الأصلية بعد فترة الانتظار
    const later = () => {
      clearTimeout(timeout); // بنلغي أي timeout قديم
      func(...args); // بننفذ الفانكشن الأصلية وندخل لها الـ arguments
    };

    // الخطوة الأولى هنا هي إلغاء أي timeout قديم لو كان موجود
    clearTimeout(timeout);

    // هنا بنعيّن timeout جديد باستدعاء الفانكشن `later` بعد فترة الانتظار `wait`
    timeout = setTimeout(later, wait);
  };
}

// مثال للاستخدام
const searchInput = document.querySelector('#search'); // بنجيب عنصر input الخاص بالبحث من الـ DOM
const debouncedSearch = debounce((query) => {
  // دي الفانكشن اللي هتنفذ بعد فترة انتظار مع تطبيق debounce
  fetchSearchResults(query); // بنبعت الطلب للـ API عشان نجيب نتائج البحث
}, 500);

// بنضيف event listener على الـ input عشان يتابع الكتابة
searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value); // كل ما يكتب المستخدم حرف، نستدعي الفانكشن debouncedSearch
});

الفكرة الأساسية هنا هي إننا عايزين نأخر تنفيذ الفانكشن الأصلية (fetchSearchResults) لحد ما المستخدم يخلص الكتابة في حقل البحث. ده عشان نقلل من عدد المرات اللي بنبعت فيها طلبات للسيرفر. إزاي بيحصل ده؟

  1. أول حاجة بنعرف متغير timeout عشان نستخدمه لتخزين معرف الـ setTimeout اللي هنعملها.

  2. بنرجع فانكشن جديدة داخل فانكشن debounce.
    دي الفانكشن اللي هتتنفذ بدل الفانكشن الأصلية.

  3. داخل الفانكشن الجديدة، بنعرف فانكشن داخلية اسمها later.
    دي الفانكشن اللي هتنفذ الفانكشن الأصلية بعد فترة الانتظار.

  4. أول حاجة في later هي clearTimeout(timeout).
    ده عشان نلغي أي setTimeout قديم موجود.

  5. بعدين بننفذ الفانكشن الأصلية func ونمرر لها الـ arguments اللي اتبعتت.

  6. قبل ما ننفذ later، بنلغي أي setTimeout قديم موجود بـ clearTimeout(timeout).

  7. بعدين بنحدد setTimeout جديد باستدعاء later وبنمرر لها فترة الانتظار wait.

2. Throttling (التقييد):#

“Throttling” دي تقنية بنستخدمها في جافاسكريبت عشان نحدد عدد مرات تنفيذ فانكشن معينة في فترة زمنية محددة. ده بيبقى مفيد في حالات زي التمرير (Scroll) مثلاً.
لما الواحد بيعمل تمرير على الصفحة، مش عاوزين نحدث الواجهة كل ما الواحد يحرك الماوس، ده هيكلف الموارد وهيأثر على أداء التطبيق
فبنستخدم Throttling عشان نحدد أقصى عدد مرات ممكن ننفذ فيها الفانكشن اللي بتحدث الواجهة في فترة زمنية معينة.
هنا مثال على طريقة تنفيذ Throttling في جافاسكريبت:

function throttle(func, limit) {
  let inThrottle; // هنا بنعرف متغير بنستخدمه لتتبع حالة الـ Throttling

  return function(...args) {
    // بنتأكد إن الـ Throttling مش في وضع "تشغيل" حاليًا
    if (!inThrottle) {
      // لو مش في وضع "تشغيل"، بننفذ الفانكشن الأصلية
      func.apply(this, args);
      // وبنغير حالة الـ Throttling لـ "تشغيل"
      inThrottle = true;

      // بعد كده، بنستخدم setTimeout عشان نغير حالة الـ Throttling لـ "إيقاف" بعد فترة الـ limit
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// مثال للاستخدام
const onScroll = throttle(() => {
  // تحديث UI بناءً على السكرول
  updateUI();
}, 250);

// بنضيف event listener على التمرير (Scroll)
window.addEventListener('scroll', onScroll);

في المثال ده، عندنا فانكشن throttle :

func: دي الفانكشن اللي عاوزين ننفذها.
limit: دي الحد الأقصى للفترة بالمللي ثانية اللي لازم تمر قبل ما نسمح بتنفيذ الفانكشن تاني
لما بنستدعي onScroll، مش هيتم استدعاء الفانكشن الأصلية updateUI إلا مرة كل 250 مللي ثانية، بغض النظر عن عدد مرات حدوث حدث التمرير. يعني لو الواحد كان بيعمل تمرير بسرعة، مش هيتم تحديث الواجهة إلا بمعدل 4 مرات في الثانية (1000 مللي ثانية / 250 مللي ثانية). طب Throttling بتشتغل إزاي؟

أول حاجة بنعرف متغير inThrottle عشان نستخدمه لتتبع حالة الـ Throttling. بنرجع فانكشن جديدة داخل فانكشن throttle. دي الفانكشن اللي هتتنفذ بدل الفانكشن الأصلية.
في الفانكشن الجديدة، بنتأكد إن الـ Throttling مش في وضع “تشغيل” حاليًا (يعني inThrottle = false).
لو الـ Throttling مش في وضع “تشغيل”، بننفذ الفانكشن الأصلية func ونغير حالة الـ Throttling لـ “تشغيل” (inThrottle = true). بعد كده، بنستخدم setTimeout عشان نغير حالة الـ Throttling لـ “إيقاف” بعد فترة الـ limit.

في المثال، الفترة ده هي 250 مللي ثانية. يعني لو الواحد كان بيعمل تمرير بسرعة، مش هيتم تحديث الواجهة إلا بمعدل 4 مرات في الثانية.

3. Event Delegation (تفويض الأحداث):#

الفكرة الأساسية تفويض الأحداث ده أسلوب بنستخدمه عشان نتعامل مع الأحداث (زي الكليك مثلاً) بطريقة أكفأ. بدل ما نضيف event listener لكل عنصر، بنضيف event listener واحد للعنصر الأب وبنستفيد من خاصية الـ event bubbling.
تخيل معايا إن عندنا قائمة فيها مجموعة من المنتجات في موقع إلكتروني:

<div class="products-list">
    <div class="product-item" data-id="1">منتج 1</div>
    <div class="product-item" data-id="2">منتج 2</div>
    <div class="product-item" data-id="3">منتج 3</div>
    <!-- ممكن يكون فيه 100 منتج أو أكتر -->
</div>
//✅ الطريقة الصح (باستخدام Event Delegation)

// بنضيف event listener واحد بس للعنصر الأب
document.querySelector('.products-list').addEventListener('click', (e) => {
    // بنتأكد إن العنصر اللي اتعمله كليك هو فعلاً منتج
    if (e.target.matches('.product-item')) {
        // بنجيب ID المنتج من الـ data attribute
        const productId = e.target.dataset.id;
        // بنعمل اللي احنا عايزينه مع المنتج
        handleProductClick(productId);
    }
});

function handleProductClick(productId) {
    alert(`تم الضغط على المنتج رقم ${productId}`);
}

// ❌ الطريقة غير المفضلة (بدون Event Delegation)
// دي طريقة مش كويسة لأنها بتضيف event listener لكل منتج
document.querySelectorAll('.product-item').forEach(item => {
    item.addEventListener('click', () => {
        const productId = item.dataset.id;
        handleProductClick(productId);
    });
});

ليه بنستخدم Event Delegation؟

  • أداء أفضل: بدل ما نضيف event listeners كتير، بنضيف واحد بس.
  • ذاكرة أقل: كل event listener بياخد مساحة في الذاكرة.
  • مرونة أكتر: لو ضفنا عناصر جديدة للقائمة، مش محتاجين نضيف event listeners جديدة.
  • كود أنضف: الكود بيبقى أسهل في الفهم والصيانة.

نصايح مهمة

  • استخدم e.target.matches() للتأكد من العنصر اللي اتعمله كليك.
  • استخدم e.target.closest() للوصول لأقرب عنصر أب بـclass معين.
  • ممكن تستخدم data-* attributes لتخزين بيانات إضافية في العناصر.

7. تحسين الأنيميشن#

1. استخدام CSS بدل JavaScript:#

/* ✅ أنيميشن CSS */
.element {
    transform: translateX(0);
    transition: transform 0.3s ease;
    will-change: transform;
}

.element.moved {
    transform: translateX(100px);
}

بص يا هندسة، الـCSS Animation بيشتغل على الـGPU (كارت الشاشة) على طول. يعني إيه؟

هو زي ما تكون عندك عربية بتشتغل على بنزين 95 - أسرع وأنضف المتصفح بيعرف قبلها إن في حركة هتحصل، فبيجهز نفسه مش بيعمل reflow (إعادة حساب) للصفحة كلها

خد بالك من will-change دي مهمة جداً:

will-change: transform;
  • دي زي ما تكون بتقول للمتصفح “خلي بالك، العنصر ده هيتحرك”، فبيجهزله layer خاص.
  • استخدم transform و opacity بس لو ممكن
  • اوعى تغير width, height, margin - دول بيعملوا reflow
  • خلي الـanimation قصير (300ms-500ms)

2. javascript Animation#

دي بتشتغل على الـ main thread (CPU)، يعني:

  • زي ما تكون بتشغل عربية على بنزين 80 - أبطأ شوية
  • كل frame بيحتاج يحسب من الأول
  • ممكن تسبب jank (تقطيع) لو الصفحة فيها حاجات كتير

3. استخدام requestAnimationFrame:#

أولاً: إيه هو الـ requestAnimationFrame ببساطة؟ 🤔

تخيل إنك بتعمل فيلم كرتون بالطريقة القديمة. عشان تعمل حركة سلسة، لازم ترسم 60 صورة في الثانية. الـ requestAnimationFrame بيعمل نفس الفكرة بس في المتصفح.

ليه نستخدمه بدل الطرق التانية؟ 🎯

setTimeout/setInterval (❌ مش كويس)

// طريقة قديمة ومش كويسة
setInterval(() => {
    element.style.left = '100px';
}, 16); // تقريباً 60 مرة في الثانية
  • مش مزامن مع refresh rate بتاع الشاشة
  • بياخد CPU كتير
  • ممكن يعمل تقطيع في الحركة

requestAnimationFrame (✅ الطريقة المثالية)

  • بيتزامن مع الـ refresh rate بتاع الشاشة
  • بيوفر في الـ CPU
  • الحركة بتبقى سلسة جداً
function animate(element, start, end, duration) {
    const startTime = performance.now();
    
    function update(currentTime) {
        const elapsed = currentTime - startTime;
        const progress = Math.min(elapsed / duration, 1);
        
        const value = start + (end - start) * progress;
        element.style.transform = `translateX(${value}px)`;
        
        if (progress < 1) {
            requestAnimationFrame(update);
        }
    }
    
    requestAnimationFrame(update);
}

// استخدام
animate(element, 0, 100, 1000); // تحريك من 0 إلى 100 خلال ثانية

المميزات الذهبية للـ requestAnimationFrame 🏆#

  • بيتزامن مع المتصفح

بيشتغل قبل الـ render بتاع المتصفح مباشرة

بيضمن إن الحركة هتكون smooth

  • بيوفر في البطارية

لما تقفل التاب أو تنزل الصفحة، بيوقف تلقائي

في الموبايل ده مهم جداً للبطارية

  • ذكي في التعامل مع الـ frames

بيتجنب frame dropping

بيظبط نفسه مع refresh rate الشاشة

  • ذكي في التعامل مع الـ frames

بيتجنب frame dropping

بيظبط نفسه مع refresh rate الشاشة

خلينا نشوف مثال عملي يوضح الفرق:

import React, { useState, useRef, useEffect } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';

const AnimationComparison = () => {
  const [isRunningRAF, setIsRunningRAF] = useState(false);
  const [isRunningInterval, setIsRunningInterval] = useState(false);
  
  const rafBoxRef = useRef(null);
  const intervalBoxRef = useRef(null);
  const frameCountRef = useRef(0);
  const startTimeRef = useRef(0);
  const animationRef = useRef(null);
  const intervalRef = useRef(null);

  const animateWithRAF = (timestamp) => {
    if (!startTimeRef.current) {
      startTimeRef.current = timestamp;
    }
    
    const progress = timestamp - startTimeRef.current;
    const x = (Math.sin(progress / 1000) * 100) + 100;
    
    if (rafBoxRef.current) {
      rafBoxRef.current.style.transform = `translateX(${x}px)`;
    }
    
    frameCountRef.current++;
    
    if (isRunningRAF) {
      animationRef.current = requestAnimationFrame(animateWithRAF);
    }
  };

  const animateWithInterval = () => {
    intervalRef.current = setInterval(() => {
      const progress = Date.now() - startTimeRef.current;
      const x = (Math.sin(progress / 1000) * 100) + 100;
      
      if (intervalBoxRef.current) {
        intervalBoxRef.current.style.transform = `translateX(${x}px)`;
      }
    }, 16); // roughly 60fps
  };

  const startRAF = () => {
    setIsRunningRAF(true);
    frameCountRef.current = 0;
    startTimeRef.current = 0;
    requestAnimationFrame(animateWithRAF);
  };

  const stopRAF = () => {
    setIsRunningRAF(false);
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current);
    }
  };

  const startInterval = () => {
    setIsRunningInterval(true);
    startTimeRef.current = Date.now();
    animateWithInterval();
  };

  const stopInterval = () => {
    setIsRunningInterval(false);
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
  };

  useEffect(() => {
    return () => {
      stopRAF();
      stopInterval();
    };
  }, []);

  return (
    <div className="p-4 space-y-8">
      <div className="space-y-4">
        <h3 className="text-lg font-bold">RequestAnimationFrame</h3>
        <div className="space-x-2">
          <Button onClick={startRAF} disabled={isRunningRAF}>
            شغل RAF
          </Button>
          <Button onClick={stopRAF} disabled={!isRunningRAF}>
            وقف RAF
          </Button>
        </div>
        <Card 
          ref={rafBoxRef}
          className="w-32 h-32 bg-blue-500"
        >
          <CardContent className="flex items-center justify-center text-white">
            RAF Box
          </CardContent>
        </Card>
      </div>

      <div className="space-y-4">
        <h3 className="text-lg font-bold">SetInterval</h3>
        <div className="space-x-2">
          <Button onClick={startInterval} disabled={isRunningInterval}>
            شغل Interval
          </Button>
          <Button onClick={stopInterval} disabled={!isRunningInterval}>
            وقف Interval
          </Button>
        </div>
        <Card 
          ref={intervalBoxRef}
          className="w-32 h-32 bg-green-500"
        >
          <CardContent className="flex items-center justify-center text-white">
            Interval Box
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default AnimationComparison;

خد المثال دا واعمله run عندك

8. نصائح مهمة عند استخدام requestAnimationFrame 🎯#

اعمل cleanup

const animationId = requestAnimationFrame(animate);
// لما تخلص
cancelAnimationFrame(animationId);

احسب الـ delta time

let lastTime = 0;

function animate(currentTime) {
    const deltaTime = currentTime - lastTime;
    lastTime = currentTime;
    
    // استخدم deltaTime في الحركة
    position += speed * deltaTime;
    
    requestAnimationFrame(animate);
}

تجنب العمليات الثقيلة

function animate() {
    // ❌ مش كويس
    heavyCalculation();
    
    // ✅ كويس
    if (needsUpdate) {
        lightweightUpdate();
    }
    
    requestAnimationFrame(animate);
}

متى متستخدمش requestAnimationFrame؟ 🚫

  • للحركات البسيطة (CSS أفضل)
  • للتحديثات اللي مش مرتبطة بالـ visual (استخدم setTimeout)
  • لو محتاج تعمل حركة مرة واحدة وبس (CSS transitions أفضل)
  • في المثال اللي فوق، جرب الحركتين وشوف الفرق بنفسك. هتلاحظ إن الـ RAF أنعم وأحسن في الأداء!

9. استخدام Web Workers#

أولاً: إيه هو الـ Web Worker أصلاً؟ 🤔#

تخيل إنك في مطعم، وعندك:

الويتر (الـ Main Thread) اللي بيتعامل مع الزباين والطباخين (الـ Workers) اللي في المطبخ

الـ Web Worker زي الطباخ بالظبط - بيشتغل في الباك جراوند من غير ما يعطل الويتر عن شغله.

ليه نستخدم Web Workers؟ 🎯#

  • منع تجميد الصفحة

  • لما تعمل عمليات معقدة

  • تحليل ملفات كبيرة

  • عمليات حسابية كتير

  • تحسين الأداء

  • شغل موازي (Parallel)

  • استغلال كل الـ CPU cores

إتكلمت عن الـ Web Workers بالتفصيل هنا 🤔#

إدارة الذاكرة#

أولاً: موضوع الـEvent Listeners ده:#

تخيل معايا إنك في مول كبير، وعامل حفلة، وحاطط ناس (Event Listeners) على كل باب تراقب مين بيدخل:

let visitorsCounter = 0;

function watchVisitor() {
    visitorsCounter++;
    console.log("زائر جديد دخل المول");
}

// حطينا مراقب على الباب
door.addEventListener('open', watchVisitor);

المشكلة هنا إيه؟ لو قفلت المول ومشيت الناس اللي على الأبواب، لازم تشيلهم… لو سيبتهم:

  • هيفضلوا واقفين على الفاضي (ياكلوا من memory بتاع الجهاز)
  • هيفضلوا يعدوا الناس حتى بعد ما المول قفل (يعملوا calculations ملهاش لازمة)

الحل:

class Mall {
    constructor() {
        this.watchVisitor = this.watchVisitor.bind(this);
        this.openMall();
    }
    
    openMall() {
        door.addEventListener('open', this.watchVisitor);
    }
    
    closeMall() {
        // شيل المراقبين قبل ما تقفل
        door.removeEventListener('open', this.watchVisitor);
    }
}

2. ثانياً: موضوع الـMemory Leaks:#

ده زي ما تكون عندك مطعم وبتخزن أوردرات الزباين:

   class Restaurant {
    constructor() {
        this.orders = new Map();
        // كل دقيقة هننضف الأوردرات القديمة
        this.cleanupTimer = setInterval(() => this.cleanOldOrders(), 60000);
    }
    
    addOrder(orderId, order) {
        this.orders.set(orderId, {
            ...order,
            timestamp: Date.now()
        });
    }
    
    cleanOldOrders() {
        const now = Date.now();
        // شيل كل الأوردرات اللي عدى عليها ساعة
        for (const [orderId, order] of this.orders.entries()) {
            if (now - order.timestamp > 3600000) {
                this.orders.delete(orderId);
            }
        }
    }
    
    closeRestaurant() {
        // وقف التايمر
        clearInterval(this.cleanupTimer);
        // فضي كل الأوردرات
        this.orders.clear();
        this.orders = null;
    }
}

كل ما تسيب Event Listeners من غير ما تشيلهم، الذاكرة بتاعت البراوزر هتزيد لو عندك 1000 صفحة كل واحدة فيها 10 listeners مش متشالين = 10,000 حتة في الذاكرة مش محتاجينها

سرعة التطبيق (Speed):

كل Event Listener بيعمل calculations تخيل معايا لو عندك زر، وكل ما تدوس عليه بيشغل 100 listener قديمين مش محتاجينهم التطبيق هيبقى بطيء على الفاضي

تأثير ده على المستخدم:

البراوزر هياكل RAM أكتر الصفحة هتبقى أبطأ في الاستجابة ممكن تعلق خالص لو المشكلة كبرت

نصايح عملية للـperformance:#

دايماً اعمل cleanup:#

class VideoPlayer {
    constructor() {
        this.video = document.querySelector('video');
        this.handlePlay = this.handlePlay.bind(this);
        this.startPlayer();
    }

    startPlayer() {
        this.video.addEventListener('play', this.handlePlay);
    }

    destroyPlayer() {
        // متنساش تشيل الـlistener
        this.video.removeEventListener('play', this.handlePlay);
        this.video = null;
    }
}

استخدم WeakMap لو محتاج تخزن references:#

class Cache {
    constructor() {
        // WeakMap بيشيل الداتا أوتوماتيك لما مفيش حد بيستخدمها
        this.cache = new WeakMap();
    }
}

راقب الـmemory usage:#

// في development
console.log(window.performance.memory);
ده بيديك فكرة عن:#
  • usedJSHeapSize: الميموري اللي البرنامج بياكله

  • totalJSHeapSize: إجمالي الميموري المتاح

  • نصيحة أخيرة: لما تعمل Component جديد، اسأل نفسك:

محتاج أشيل إيه لما الـcomponent ده يموت؟

فين الـreferences اللي ممكن تفضل معلقة؟

إيه الـtimers أو الـintervals اللي لازم أوقفها؟

تحسين التحميل#

1. Lazy Loading#

تخيل معايا إنك بتفتح فيسبوك، مش منطقي يحمل كل الصور مرة واحدة صح؟ ده زي ما تكون رايح السوبر ماركت وشايل كل الحاجات مرة واحدة… هتتعب على الفاضي!

// ✅ تحميل الصور بشكل كسول
function lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]');
    
    const imageObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });

    images.forEach(img => imageObserver.observe(img));
}

// ✅ تحميل المكونات بشكل كسول
// بدل ما نستورد كل حاجة في الأول
import HeavyComponent from './HeavyComponent';

// نعمل كده
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

// وبعدين نستخدمه كده
function MyApp() {
    return (
        <Suspense fallback={<div>جاري التحميل...</div>}>
            <HeavyComponent />
        </Suspense>
    );
}

2. Code Splitting:#

ده زي ما تكون عندك مطعم، مش هتجيب كل المكونات من المخزن… هتجيب اللي محتاجه وقت ما تحتاجه.


// ✅ تقسيم الكود (Code Splitting)
// ملف app.js الرئيسي
async function loadAdminPanel() {
    try {
        // هنحمل لوحة التحكم بس لما المدير يحتاجها
        const adminModule = await import('./admin/adminPanel.js');
        adminModule.initAdminPanel();
    } catch (error) {
        console.error('مشكلة في تحميل لوحة التحكم:', error);
    }
}

// لما المدير يدوس على الزر
document.querySelector('#adminButton').addEventListener('click', async () => {
    const userIsAdmin = checkAdminStatus();
    if (userIsAdmin) {
        await loadAdminPanel();
    }
});

3. مثال عملي للـPerformance Metrics:#

// قياس وقت تحميل المكونات
console.time('loadComponent');

const HeavyFeature = await import('./heavyFeature.js');
console.timeEnd('loadComponent');  // "loadComponent: 234ms"

// قياس حجم البيانات
const beforeLoad = window.performance.memory.usedJSHeapSize;
await loadModule();
const afterLoad = window.performance.memory.usedJSHeapSize;
console.log(`استهلاك الذاكرة: ${(afterLoad - beforeLoad) / 1024 / 1024} MB`);

تابع تحسين التحميل#

4. Bundling & Minification:#

Bundling#

ده زي إنك عندك محل هدوم وعايز تنقل البضاعة: ده زي ما تجمع كل الهدوم في شنطة واحدة كبيرة بدل ما تنقلهم قطعة قطعة.

// قبل الـBundling
import header from './header.js';  // 50KB
import footer from './footer.js';  // 30KB
import sidebar from './sidebar.js';  // 40KB

// بعد الـBundling
// كله في فايل واحد bundle.js - 100KB (أصغر من مجموع الحجم الأصلي)

Minification#

ده زي ما تضغط الهدوم في الشنطة عشان تاخد مساحة أقل:

// قبل الـMinification
function calculateTotal(items) {
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total = total + items[i].price;
    }
    return total;
}

// بعد الـMinification
function c(i){let t=0;for(let x=0;x<i.length;x++)t+=i[x].price;return t}

التكوين العملي في Webpack:

// webpack.config.js
// webpack.config.js بالبلدي
module.exports = {
    // نمط الإنتاج - يعني شغل كل التحسينات
    mode: 'production',
    
    optimization: {
        // تقسيم الباندلز
        splitChunks: {
            chunks: 'all',
            // أقل حجم للملف قبل ما نقسمه
            minSize: 20000, // 20KB
            
            // تجميع المكتبات الخارجية في ملف منفصل
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                }
            }
        }
    }
};

4. تحسين الـ Critical Rendering Path:#

تخيل إنك بتفتح مطعم:

الحاجات الأساسية (Critical CSS):

لازم تجهز المطبخ والطرابيزات الأول

بعدين تجهز الديكورات والحاجات الإضافية



<!-- الحاجات المهمة أول ما الصفحة تفتح -->
<head>
    <!-- الأساسي اللي محتاجينه على طول -->
    <style>
        .header { background: #fff; }
        .main-nav { display: flex; }
        .hero { height: 400px; }
    </style>
    
    <!-- الحاجات اللي مش أساسية نحملها بعدين -->
    <link 
        rel="preload" 
        href="animations.css" 
        as="style" 
        onload="this.rel='stylesheet'"
    >
</head>
<!-- ✅ تحميل CSS المهم فوراً -->
<link rel="stylesheet" href="critical.css">

<!-- ✅ تحميل CSS غير المهم بشكل مؤجل -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">

<!-- ✅ تحميل JavaScript بشكل مؤجل -->
<script defer src="app.js"></script>

استخدم Resource Hints:#

  1. rel=“preconnect” 🔌 تخيل إنك عايز تتصل بصاحبك:

بتطلب رقمه (DNS lookup) بتعمل اتصال (TCP handshake) بتتأكد إن الخط آمن (SSL) الـpreconnect بيعمل كل ده قبل ما تحتاج الـAPI أصلاً. ده بيوفر وقت لما تيجي تستخدم الـAPI فعلاً.

<link rel="preconnect" href="https://api.example.com">
  1. rel=“dns-prefetch” 🔍 ده زي ما تكون بتدور على عنوان قبل ما تروح المكان. بيجيب الـIP address بتاع الـdomain قبل ما تحتاجه.
<link rel="dns-prefetch" href="https://images.example.com">

متى نستخدم كل واحد؟

<!-- استخدم preconnect للمواقع اللي هتحتاجها على طول -->
<link rel="preconnect" href="https://main-api.com">

<!-- استخدم dns-prefetch للمواقع اللي ممكن تحتاجها -->
<link rel="dns-prefetch" href="https://optional-service.com">
  1. rel=“preload” 📦
<link rel="preload" href="critical-font.woff2" as="font">

ده زي ما تجيب الحاجة قبل ما تحتاجها. بيقول للبراوزر “حمل الفايل ده دلوقتي لأننا هنحتاجه قريب”.

التخزين المؤقت (Caching)#

1. استخدام Service Worker:#

ده زي واحد بواب شاطر، واقف على باب التطبيق بتاعك:

بيحفظ نسخة من كل حاجة مهمة لو النت ضعيف أو مفيش نت، بيجيب الحاجات المحفوظة بيتحكم في طلبات الـnetwork

// service-worker.js
// 1. تسجيل Service Worker
// في الصفحة الرئيسية (index.html)
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(reg => console.log('البواب جه 😎'))
            .catch(err => console.log('البواب مجاش 😢', err));
    });
}

// 2. إعداد Service Worker (sw.js)
const CACHE_NAME = 'myApp-v1';
const ASSETS_TO_CACHE = [
    '/',
    '/index.html',
    '/styles.css',
    '/app.js',
    '/images/logo.png',
    '/offline.html'
];

// لما البواب يبدأ شغله
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('بحفظ الملفات المهمة 📦');
                return cache.addAll(ASSETS_TO_CACHE);
            })
    );
});

// لما حد يطلب حاجة
self.addEventListener('fetch', (event) => {
    event.respondWith(
        // نشوف لو عندنا نسخة محفوظة
        caches.match(event.request)
            .then(cachedResponse => {
                // لو لقينا نسخة، نديها على طول
                if (cachedResponse) {
                    console.log('جبناها من المخزن 🏪');
                    return cachedResponse;
                }

                // لو ملقيناش، نجيبها من النت ونحفظها
                return fetch(event.request)
                    .then(response => {
                        // نحفظ نسخة للمرة الجاية
                        const responseToCache = response.clone();
                        caches.open(CACHE_NAME)
                            .then(cache => {
                                cache.put(event.request, responseToCache);
                            });
                        return response;
                    })
                    .catch(() => {
                        // لو مفيش نت، نرجع صفحة الـoffline
                        if (event.request.mode === 'navigate') {
                            return caches.match('/offline.html');
                        }
                    });
            })
    );
});

2. استخدام IndexedDB للتخزين المحلي:#

ده زي مخزن كبير في جهاز المستخدم، بنخزن فيه داتا كتير:

ممكن نخزن فيه صور ممكن نخزن فيه بيانات المستخدم ممكن نخزن فيه أي حاجة عايزين نرجعلها بعدين

class SuperMarketDB {
    constructor() {
        this.dbName = 'superMarket';
        this.db = null;
    }

    // نفتح المخزن
    async connect() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(this.dbName, 1);

            request.onerror = () => {
                console.error('مقدرناش نفتح المخزن 😢');
                reject(request.error);
            };

            request.onsuccess = () => {
                console.log('فتحنا المخزن 🎉');
                this.db = request.result;
                resolve();
            };

            // لو أول مرة نفتح المخزن
            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                
                // مخزن للمنتجات
                const productsStore = db.createObjectStore('products', { keyPath: 'id' });
                productsStore.createIndex('name', 'name', { unique: false });
                
                // مخزن للطلبات
                const ordersStore = db.createObjectStore('orders', { keyPath: 'id', autoIncrement: true });
                ordersStore.createIndex('date', 'date', { unique: false });
            };
        });
    }

    // نحفظ منتج
    async saveProduct(product) {
        return this._executeTransaction('products', 'readwrite', (store) => {
            store.put(product);
        });
    }

    // نجيب منتج
    async getProduct(id) {
        return this._executeTransaction('products', 'readonly', (store) => {
            return store.get(id);
        });
    }

    // نحفظ طلب
    async saveOrder(order) {
        return this._executeTransaction('orders', 'readwrite', (store) => {
            order.date = new Date();
            return store.add(order);
        });
    }

    // نجيب كل الطلبات
    async getAllOrders() {
        return this._executeTransaction('orders', 'readonly', (store) => {
            return store.getAll();
        });
    }

    // helper function للتعامل مع المخزن
    async _executeTransaction(storeName, mode, callback) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction(storeName, mode);
            const store = transaction.objectStore(storeName);

            let request = callback(store);

            transaction.oncomplete = () => resolve(request?.result);
            transaction.onerror = () => reject(transaction.error);
        });
    }
}

// استخدام المخزن
async function demoSuperMarket() {
    const db = new SuperMarketDB();
    await db.connect();

    // نحفظ منتجات
    await db.saveProduct({
        id: 1,
        name: 'موز',
        price: 15,
        quantity: 100
    });

    // نحفظ طلب
    await db.saveOrder({
        items: [
            { productId: 1, quantity: 3 }
        ],
        totalPrice: 45
    });

    // نجيب الطلبات
    const orders = await db.getAllOrders();
    console.log('كل الطلبات:', orders);
}

استخدم Service Worker للـ:

  • ملفات الـHTML/CSS/JS الأساسية
  • الصور المتكررة
  • API responses المهمة

استخدم IndexedDB للـ:

  • داتا كبيرة (صور، فيديوهات)
  • بيانات المستخدم المهمة
  • الداتا اللي محتاجين نعمل عليها search
// نضيف تنظيف دوري للكاش القديم
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys()
            .then(cacheNames => {
                return Promise.all(
                    cacheNames
                        .filter(name => name !== CACHE_NAME)
                        .map(name => caches.delete(name))
                );
            })
    );
});

النصيحة الأخيرة:

  • اعمل خطة للـcaching strategy
  • خلي عندك fallback content
  • راقب حجم الـcache
  • نظف الـcache القديم
  • اختبر التطبيق offline

3. استراتيجيات التخزين المؤقت:#

Cache First Strategy:#

تخيل إنك عندك محل بيتزا، و Cache First ده زي إنك:

لما حد يطلب بيتزا، أول حاجة بتبص في التلاجة (الكاش) لو لقيت البيتزا الجاهزة في التلاجة، تديهاله على طول لو مش موجودة، تروح تعملها (تجيبها من النت) وتحط نسخة في التلاجة للمرة الجاية

// لما حد يطلب حاجة
async function cacheFirst(request) {
    // بنبص في التلاجة الأول
    const cachedResponse = await caches.match(request);
    // لو لقينا حاجة، نديهاله على طول
    if (cachedResponse) {
        return cachedResponse;
    }
    
    // لو مش موجودة، نجيبها من النت
    const networkResponse = await fetch(request);
    // ونحط نسخة في التلاجة
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, networkResponse.clone());
    return networkResponse;
}

Network First Strategy:#

ودي زي إنك:

بتروح تجيب البيتزا الطازة من المطبخ (النت) الأول لو المطبخ مقفول (النت مش شغال)، بتجيب من التلاجة (الكاش) لو مفيش في التلاجة كمان، تقول للزبون معلش مفيش دلوقتي

// لما حد يطلب حاجة
async function networkFirst(request) {
    try {
        // نجرب نجيب من النت الأول
        const networkResponse = await fetch(request);
        // نحط نسخة في التلاجة للطوارئ
        const cache = await caches.open(CACHE_NAME);
        cache.put(request, networkResponse.clone());
        return networkResponse;
    } catch (error) {
        // لو النت فصل، نشوف التلاجة
        const cachedResponse = await caches.match(request);
        if (cachedResponse) {
            return cachedResponse;
        }
        // لو مفيش ولا كده ولا كده، نقول معلش
        throw error;
    }
}

نصائح إضافية للأداء:#

  1. تقليل عدد الـ Re-renders في React:
    • استخدام Profiler تخيل إنك عندك مطعم وعايز تعرف أي طلب بياخد وقت كتير في المطبخ:
// قبل التحسين
const MenuItem = ({ item, price, description }) => {
  // كل حاجة بتتعمل من جديد كل مرة 😱
  const calculateDiscount = () => {
    return price * 0.9;  // خصم 10%
  };

  return (
    <div>
      <h3>{item}</h3>
      <p>{description}</p>
      <p>السعر: {calculateDiscount()}</p>
    </div>
  );
};

// بعد التحسين
const MenuItem = memo(({ item, price, description }) => {
  // استخدام useMemo للحسابات المعقدة
  const discountedPrice = useMemo(() => {
    return price * 0.9;
  }, [price]);

  return (
    <div>
      <h3>{item}</h3>
      <p>{description}</p>
      <p>السعر: {discountedPrice}</p>
    </div>
  );
});
  • استخدام الـ key بشكل صحيح في React إتكلمت عنها بالتفصيل هنا
  1. تحسين الـ Network Performance:
    • ضغط الملفات مثل الصور
// في ملف server.js
const express = require('express');
const sharp = require('sharp');
const path = require('path');

const app = express();

// ميدلوير لمعالجة الصور
app.get('/images/:imageName', async (req, res) => {
    try {
        const imagePath = path.join(__dirname, 'uploads', req.params.imageName);
        
        // جيبلي العرض والطول من الكويري
        const width = parseInt(req.query.width) || 800;
        const quality = parseInt(req.query.quality) || 80;
        
        // معالجة الصورة
        const processedImage = await sharp(imagePath)
            .resize(width, null, {
                fit: 'contain',
                withoutEnlargement: true
            })
            .webp({ quality }) // تحويل للويب بي
            .toBuffer();
            
        res.set('Content-Type', 'image/webp');
        res.send(processedImage);
        
    } catch (error) {
        console.error('حصل مشكلة:', error);
        res.status(500).send('حصل مشكلة في معالجة الصورة');
    }
});
  • استخدام CDN للملفات الثابتة
  1. تحسين الـ Font Loading:
    • استخدام font-display: swap
    • تحميل الخطوط بشكل مؤجل
    • استخدام preload للخطوط المهمة
<!-- الطريقة المثالية لتحميل الخطوط -->
<link rel="preload" 
      href="fonts/cairo.woff2" 
      as="font"
      type="font/woff2" 
      crossorigin>

<style>
  @font-face {
    font-family: 'Cairo';
    font-display: swap;  /* عشان النص يظهر على طول */
    src: url('fonts/cairo.woff2') format('woff2');
  }
</style>
  1. تحسين الـ Images:
    • استخدام الصور المتجاوبة مع srcset
    • ضغط الصور بشكل ذكي
    • استخدام تنسيقات الصور الحديثة مثل WebP
    • استخدم next/image في Next.js
    • اعمل lazy loading للصور البعيدة عن الشاشة
    • استخدم placeholder للصور لحد ما تتحمل
<!-- صور متجاوبة -->
<img src="small.jpg"
     srcset="small.jpg 300w,
             medium.jpg 600w,
             large.jpg 900w"
     sizes="(max-width: 600px) 300px,
            (max-width: 900px) 600px,
            900px"
     loading="lazy"
     alt="وصف الصورة">

أدوات قياس الأداء:#

  1. Chrome DevTools Performance Panel (المتصفح) 🎯

بتفتحها عن طريق:

  • F12 في المتصفح
  • أو Right Click -> Inspect -> Performance
// يمكنك قياس الأداء برمجياً
performance.mark('start');
// كودك هنا
performance.mark('end');

performance.measure('myOperation', 'start', 'end');

// طباعة النتائج
performance.getEntriesByType('measure').forEach(measure => {
  console.log(`${measure.name}: ${measure.duration}ms`);
});
  1. Lighthouse CI

التثبيت

npm install -g @lhci/cli
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 3,
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000']
    },
    assert: {
      assertions: {
        'first-contentful-paint': ['warn', {maxNumericValue: 2000}],
        'interactive': ['error', {maxNumericValue: 5000}]
      }
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
};
  1. Web Vitals
npm install web-vitals
import {onCLS, onFID, onLCP, onTTFB, onFCP} from 'web-vitals';

function sendToAnalytics({name, delta, id}) {
  // إرسال للتحليلات
  console.log(`${name}: ${delta}ms`);
}

// قياس كل المقاييس المهمة
onCLS(sendToAnalytics);  // Cumulative Layout Shift
onFID(sendToAnalytics);  // First Input Delay
onLCP(sendToAnalytics);  // Largest Contentful Paint
onTTFB(sendToAnalytics); // Time to First Byte
onFCP(sendToAnalytics);  // First Contentful Paint

  1. WebPageTest بيديك تحليل تفصيلي للموقع يدعم اختبار من مواقع مختلفة حول العالم

  2. SpeedCurve

مراقبة مستمرة للأداء تحليلات متقدمة ومقارنات

  1. Next.js Analytics
// next.config.js
module.exports = {
  experimental: {
    performanceMetrics: true
  }
}
  1. React Profiler
// تثبيت الأداة
import { Profiler } from 'react';

<Profiler id="MyComponent" onRender={(id, phase, actualDuration) => {
  console.log(`${id} rendered in ${actualDuration}ms`);
}}>
  <MyComponent />
</Profiler>
  1. PageSpeed Insights معتمدة من جوجل

Join our whatsapp group here
My Channel here