JavaScript المتقدم (ES6+)

تحسين الأداء

13 دقيقة الدرس 38 من 40

تحسين الأداء

يُعد تحسين الأداء أمراً بالغ الأهمية لإنشاء تطبيقات سريعة وسريعة الاستجابة. في هذا الدرس، سنستكشف تقنيات لقياس وتحليل وتحسين أداء JavaScript.

قياس الأداء

قبل التحسين، تحتاج إلى قياس الأداء بدقة:

// 1. استخدام performance.now() - توقيت عالي الدقة const start = performance.now(); // الكود المراد قياسه for (let i = 0; i < 1000000; i++) { Math.sqrt(i); } const end = performance.now(); console.log(`Operation took ${end - start}ms`); // 2. استخدام console.time() - توقيت بسيط console.time('myOperation'); // الكود المراد قياسه const result = heavyComputation(); console.timeEnd('myOperation'); // myOperation: 123.45ms // 3. علامات وقياسات الأداء performance.mark('start-fetch'); fetch('https://api.example.com/data') .then(response => response.json()) .then(data => { performance.mark('end-fetch'); performance.measure('fetch-duration', 'start-fetch', 'end-fetch'); const measure = performance.getEntriesByName('fetch-duration')[0]; console.log(`Fetch took: ${measure.duration}ms`); }); // 4. واجهة برمجة تطبيقات توقيت الموارد - قياس تحميل الموارد window.addEventListener('load', () => { const resources = performance.getEntriesByType('resource'); resources.forEach(resource => { console.log(`${resource.name}: ${resource.duration}ms`); }); }); // 5. توقيت التنقل - مقاييس تحميل الصفحة const navigationTiming = performance.getEntriesByType('navigation')[0]; console.log('Page load metrics:', { domContentLoaded: navigationTiming.domContentLoadedEventEnd - navigationTiming.domContentLoadedEventStart, totalLoadTime: navigationTiming.loadEventEnd - navigationTiming.fetchStart, domInteractive: navigationTiming.domInteractive - navigationTiming.fetchStart }); // 6. المقارنة بين أساليب مختلفة function benchmark(name, fn, iterations = 10000) { const start = performance.now(); for (let i = 0; i < iterations; i++) { fn(); } const end = performance.now(); const total = end - start; const average = total / iterations; console.log(`${name}: Total: ${total.toFixed(2)}ms Average: ${average.toFixed(4)}ms per iteration `); } // مقارنة الأساليب benchmark('Array.push', () => { const arr = []; arr.push(1); }); benchmark('Array literal', () => { const arr = [1]; });
نصيحة: قس الأداء دائماً في بيئات مشابهة للإنتاج. إصدارات التطوير وأدوات المطور في المتصفح يمكن أن تؤثر على قياسات الأداء.

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

يُعد استخدام الذاكرة بكفاءة أمراً ضرورياً للأداء:

// 1. فهم جمع القمامة // يستخدم JavaScript جمع القمامة التلقائي // يتم تحرير الكائنات عندما لا توجد مراجع // سيئ: ينشئ كائنات جديدة باستمرار function processItems(items) { items.forEach(item => { const config = { // كائن جديد في كل تكرار format: 'json', timeout: 5000 }; processItem(item, config); }); } // جيد: إعادة استخدام الكائنات function processItems(items) { const config = { // كائن واحد يُعاد استخدامه format: 'json', timeout: 5000 }; items.forEach(item => { processItem(item, config); }); } // 2. تجميع الكائنات للكائنات المُنشأة بشكل متكرر class ObjectPool { constructor(factory, reset) { this.factory = factory; this.reset = reset; this.pool = []; } acquire() { return this.pool.length > 0 ? this.pool.pop() : this.factory(); } release(obj) { this.reset(obj); this.pool.push(obj); } } // الاستخدام const particlePool = new ObjectPool( () => ({ x: 0, y: 0, vx: 0, vy: 0 }), (particle) => { particle.x = 0; particle.y = 0; particle.vx = 0; particle.vy = 0; } ); // 3. تجنب تسرب الذاكرة // سيئ: مستمعو الأحداث غير المزالين class Component { constructor() { window.addEventListener('resize', this.handleResize); } handleResize() { // معالجة إعادة الحجم } } // جيد: تنظيف المستمعين class Component { constructor() { this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize); } destroy() { window.removeEventListener('resize', this.handleResize); } handleResize() { // معالجة إعادة الحجم } } // 4. WeakMap للتخزين المؤقت بدون تسرب الذاكرة const cache = new WeakMap(); function processUser(user) { if (cache.has(user)) { return cache.get(user); } const result = expensiveOperation(user); cache.set(user, result); return result; } // عندما يتم جمع كائن المستخدم بواسطة جامع القمامة، // يتم إزالة إدخال الذاكرة المؤقتة تلقائياً // 5. مراقبة استخدام الذاكرة if (performance.memory) { console.log('Memory usage:', { used: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB', total: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB', limit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB' }); }

تجنب تسريبات الذاكرة

الأنماط الشائعة التي تسبب تسريبات الذاكرة وكيفية إصلاحها:

// 1. المؤقتات المنسية // سيئ class Widget { constructor() { this.interval = setInterval(() => { this.update(); }, 1000); } update() { // تحديث الأداة } } // جيد class Widget { constructor() { this.interval = setInterval(() => { this.update(); }, 1000); } destroy() { clearInterval(this.interval); } update() { // تحديث الأداة } } // 2. الإغلاقات التي تحتفظ ببيانات كبيرة // سيئ function createHandler(data) { const largeArray = new Array(1000000).fill(data); return function handler() { // يستخدم فقط data، لكنه يحتفظ بمرجع إلى largeArray console.log(data); }; } // جيد function createHandler(data) { return function handler() { console.log(data); }; } // 3. مراجع DOM // سيئ const elements = []; function cacheElements() { const divs = document.querySelectorAll('div'); divs.forEach(div => elements.push(div)); } // تبقى العناصر في الذاكرة حتى لو تمت إزالتها من DOM // جيد function processElements() { const divs = document.querySelectorAll('div'); divs.forEach(div => { // المعالجة فوراً، لا تخزن processDiv(div); }); } // 4. المتغيرات العامة // سيئ var cache = {}; // متغير عام function getData(id) { if (!cache[id]) { cache[id] = fetchData(id); } return cache[id]; } // ينمو التخزين المؤقت إلى ما لا نهاية // جيد const cache = new Map(); const MAX_CACHE_SIZE = 100; function getData(id) { if (!cache.has(id)) { if (cache.size >= MAX_CACHE_SIZE) { // إزالة أقدم إدخال const firstKey = cache.keys().next().value; cache.delete(firstKey); } cache.set(id, fetchData(id)); } return cache.get(id); }
تحذير: يمكن أن تتراكم تسريبات الذاكرة بمرور الوقت، خاصة في تطبيقات الصفحة الواحدة. قم دائماً بتنظيف الموارد عند تدمير المكونات.

تقنيات التحسين

تقنيات عملية لتحسين الأداء:

// 1. استخدام حلقات فعالة // بطيء: Array.forEach مع دالة سهم items.forEach(item => processItem(item)); // سريع: حلقة for تقليدية for (let i = 0, len = items.length; i < len; i++) { processItem(items[i]); } // 2. تخزين أطوال المصفوفات مؤقتاً // سيئ for (let i = 0; i < array.length; i++) { // يُقرأ array.length في كل تكرار } // جيد for (let i = 0, len = array.length; i < len; i++) { // الطول مخزن مؤقتاً } // 3. تجنب العمل غير الضروري في الحلقات // سيئ for (let i = 0; i < items.length; i++) { const config = getConfig(); // يُستدعى في كل تكرار processItem(items[i], config); } // جيد const config = getConfig(); // يُستدعى مرة واحدة for (let i = 0; i < items.length; i++) { processItem(items[i], config); } // 4. استخدام البحث في الكائن بدلاً من switch // بطيء function getColor(name) { switch(name) { case 'red': return '#FF0000'; case 'blue': return '#0000FF'; case 'green': return '#00FF00'; // ... حالات أكثر } } // سريع const colors = { red: '#FF0000', blue: '#0000FF', green: '#00FF00' }; function getColor(name) { return colors[name]; } // 5. دفعة عمليات DOM // سيئ: إعادة تدفقات متعددة for (let i = 0; i < 100; i++) { const div = document.createElement('div'); div.textContent = `Item ${i}`; document.body.appendChild(div); // إعادة تدفق في كل مرة } // جيد: إعادة تدفق واحدة const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const div = document.createElement('div'); div.textContent = `Item ${i}`; fragment.appendChild(div); } document.body.appendChild(fragment); // إعادة تدفق واحدة // 6. استخدام تفويض الأحداث // سيئ: مستمعون متعددون document.querySelectorAll('.button').forEach(button => { button.addEventListener('click', handleClick); }); // جيد: مستمع واحد document.addEventListener('click', (e) => { if (e.target.matches('.button')) { handleClick(e); } }); // 7. تجنب التخطيطات المتزامنة القسرية // سيئ: يسبب خفقان التخطيط for (let i = 0; i < elements.length; i++) { elements[i].style.width = elements[i].offsetWidth + 10 + 'px'; // offsetWidth يفرض حساب التخطيط } // جيد: القراءة ثم الكتابة const widths = elements.map(el => el.offsetWidth); elements.forEach((el, i) => { el.style.width = widths[i] + 10 + 'px'; }); // 8. استخدام requestAnimationFrame للرسوم المتحركة // سيئ: استخدام setTimeout function animate() { updatePosition(); setTimeout(animate, 16); // ~60fps } // جيد: استخدام requestAnimationFrame function animate() { updatePosition(); requestAnimationFrame(animate); } // 9. تحسين تسلسل السلاسل // بطيء: تسلسل السلاسل في الحلقة let html = ''; for (let i = 0; i < 1000; i++) { html += '<div>' + i + '</div>'; } // سريع: دمج المصفوفة const parts = []; for (let i = 0; i < 1000; i++) { parts.push(`<div>${i}</div>`); } const html = parts.join(''); // 10. استخدام Web Workers للحسابات الثقيلة // الخيط الرئيسي const worker = new Worker('worker.js'); worker.postMessage({ data: largeDataset }); worker.onmessage = (e) => { console.log('Result:', e.data); }; // worker.js self.onmessage = (e) => { const result = heavyComputation(e.data); self.postMessage(result); };

التهدئة والاختناق

التحكم في معدل تنفيذ الدالة للأداء:

// 1. التهدئة - التنفيذ بعد تأخير عدم النشاط function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // الاستخدام: البحث أثناء كتابة المستخدم const searchInput = document.querySelector('#search'); const debouncedSearch = debounce((value) => { performSearch(value); }, 300); searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); }); // 2. الاختناق - التنفيذ مرة واحدة على الأكثر لكل فترة زمنية function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } // الاستخدام: معالجة حدث التمرير const throttledScroll = throttle(() => { console.log('Scroll position:', window.scrollY); }, 100); window.addEventListener('scroll', throttledScroll); // 3. اختناق متقدم مع البداية والنهاية function throttleAdvanced(func, limit, options = {}) { let timeout; let previous = 0; const { leading = true, trailing = true } = options; return function(...args) { const now = Date.now(); if (!previous && !leading) { previous = now; } const remaining = limit - (now - previous); if (remaining <= 0 || remaining > limit) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(this, args); } else if (!timeout && trailing) { timeout = setTimeout(() => { previous = leading ? Date.now() : 0; timeout = null; func.apply(this, args); }, remaining); } }; } // 4. اختناق قائم على RequestAnimationFrame function rafThrottle(func) { let rafId = null; return function(...args) { if (rafId === null) { rafId = requestAnimationFrame(() => { func.apply(this, args); rafId = null; }); } }; } // الاستخدام: معالجة التمرير السلس const rafScrollHandler = rafThrottle(() => { updateScrollPosition(); }); window.addEventListener('scroll', rafScrollHandler);

التحميل البطيء وتقسيم الكود

تحميل الكود والموارد فقط عند الحاجة:

// 1. الاستيرادات الديناميكية (تقسيم الكود) // تحميل الوحدة فقط عند الحاجة button.addEventListener('click', async () => { const module = await import('./heavy-feature.js'); module.initialize(); }); // 2. التحميل البطيء للصور const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; imageObserver.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); // HTML: <img data-src="large-image.jpg" alt="Description"> // 3. تقسيم الكود على أساس المسار const routes = { '/home': () => import('./pages/home.js'), '/about': () => import('./pages/about.js'), '/contact': () => import('./pages/contact.js') }; async function loadRoute(path) { const loader = routes[path]; if (loader) { const module = await loader(); module.render(); } } // 4. الجلب المسبق // جلب الموارد التي قد يحتاجها المستخدم قريباً function prefetchResource(url) { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; document.head.appendChild(link); } // الجلب المسبق للصفحة التالية عند التمرير فوق الرابط document.querySelectorAll('a').forEach(link => { link.addEventListener('mouseenter', () => { prefetchResource(link.href); }); }); // 5. تهيئة المكون البطيئة class ExpensiveComponent { constructor(element) { this.element = element; this.initialized = false; } init() { if (this.initialized) return; // تهيئة ثقيلة this.setupComplexFeatures(); this.loadData(); this.initialized = true; } // التهيئة عندما تكون مرئية observeVisibility() { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.init(); observer.disconnect(); } }); observer.observe(this.element); } }

تمرين عملي:

حسّن هذا الكود غير الفعال:

// كود غير فعال function updateList(items) { document.querySelector('#list').innerHTML = ''; for (let i = 0; i < items.length; i++) { const li = document.createElement('li'); li.textContent = items[i].name; li.style.color = getColorFromDatabase(items[i].type); document.querySelector('#list').appendChild(li); } }

الإصدار المحسّن:

function updateList(items) { const list = document.querySelector('#list'); // تخزين المحدد مؤقتاً const fragment = document.createDocumentFragment(); // دفعة عمليات DOM // تخزين عمليات البحث عن الألوان مؤقتاً const colorCache = new Map(); for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; const li = document.createElement('li'); li.textContent = item.name; // استخدام اللون المخزن مؤقتاً أو الجلب مرة واحدة if (!colorCache.has(item.type)) { colorCache.set(item.type, getColorFromDatabase(item.type)); } li.style.color = colorCache.get(item.type); fragment.appendChild(li); } // تحديث DOM واحد list.innerHTML = ''; list.appendChild(fragment); }

أفضل الممارسات

1. القياس أولاً: - التصنيف قبل التحسين - التركيز على الاختناقات - استخدام بيانات العالم الحقيقي 2. تحسين المسار الحرج: - تقليل عمل الخيط الرئيسي - تأجيل الكود غير الحرج - تحسين العرض الأولي 3. تقليل طلبات الشبكة: - تجميع وتصغير الكود - استخدام CDN للمكتبات - تمكين الضغط (gzip/brotli) - تنفيذ استراتيجيات التخزين المؤقت 4. تحسين الأصول: - ضغط الصور - استخدام التنسيقات الحديثة (WebP، AVIF) - التحميل البطيء للصور - استخدام الصور المستجيبة 5. تقليل إعادة التدفقات/إعادة الطلاء: - دفعة تحديثات DOM - استخدام تحويلات CSS - تجنب التخطيطات المتزامنة القسرية - استخدام will-change للرسوم المتحركة 6. استخدام واجهات برمجة التطبيقات الحديثة: - IntersectionObserver - ResizeObserver - requestAnimationFrame - Web Workers 7. نظافة الكود: - إزالة الكود غير المستخدم - تجنب تسريبات الذاكرة - تنظيف مستمعي الأحداث - مسح المؤقتات والفواصل

الملخص

في هذا الدرس، تعلمت:

  • تقنيات وأدوات قياس الأداء
  • إدارة الذاكرة وجمع القمامة
  • أنماط تسرب الذاكرة الشائعة والحلول
  • تقنيات التحسين للحلقات و DOM والسلاسل
  • التهدئة والاختناق لمعالجة الأحداث
  • استراتيجيات التحميل البطيء وتقسيم الكود
  • أفضل الممارسات لأداء JavaScript عالي
التالي: في الدرس التالي، سنستكشف واجهات برمجة تطبيقات JavaScript الحديثة التي توفر قدرات قوية لتطبيقات الويب!