الأمان والأداء
تحسين أداء JavaScript
تحسين أداء JavaScript
يؤثر أداء JavaScript بشكل مباشر على تجربة المستخدم وأوقات التحميل والتفاعلية. غالبًا ما ترسل تطبيقات الويب الحديثة حزم JavaScript كبيرة يمكن أن تبطئ تحميل الصفحة والتنفيذ. يغطي هذا الدرس تقنيات تحسين أداء JavaScript من خلال تحسين الحزم وتقسيم الكود وأنماط التنفيذ الفعالة.
تحسين حجم الحزمة
يؤدي تقليل حجم حزمة JavaScript إلى تحسين أوقات التنزيل وأداء التحليل:
<!-- قبل: حزمة واحدة كبيرة -->
<script src="/js/app.bundle.js"></script> <!-- 2.5 ميجابايت -->
<!-- بعد: أجزاء محسّنة -->
<script src="/js/vendor.js"></script> <!-- 500 كيلوبايت -->
<script src="/js/app.js"></script> <!-- 300 كيلوبايت -->
<script src="/js/lazy-features.js" defer></script> <!-- 200 كيلوبايت -->
<script src="/js/app.bundle.js"></script> <!-- 2.5 ميجابايت -->
<!-- بعد: أجزاء محسّنة -->
<script src="/js/vendor.js"></script> <!-- 500 كيلوبايت -->
<script src="/js/app.js"></script> <!-- 300 كيلوبايت -->
<script src="/js/lazy-features.js" defer></script> <!-- 200 كيلوبايت -->
ملاحظة: يمكن لأدوات التجميع الحديثة مثل Webpack و Rollup و Vite تقليل أحجام الحزم بنسبة 40-70٪ من خلال التصغير والضغط وإزالة الكود غير المستخدم.
Tree Shaking (إزالة الكود الميت)
تزيل تقنية Tree Shaking الكود غير المستخدم من حزمك. تعمل مع وحدات ES6:
// سيء: استيراد المكتبة بالكامل
import _ from 'lodash'; // 70 كيلوبايت
const result = _.debounce(fn, 300);
// جيد: استيراد ما تحتاجه فقط
import debounce from 'lodash/debounce'; // 2 كيلوبايت
const result = debounce(fn, 300);
// الأفضل: استخدام استيراد وحدات ES6
import { debounce } from 'lodash-es'; // قابل لإزالة الكود الميت
// إعداد Webpack لإزالة الكود الميت
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};
import _ from 'lodash'; // 70 كيلوبايت
const result = _.debounce(fn, 300);
// جيد: استيراد ما تحتاجه فقط
import debounce from 'lodash/debounce'; // 2 كيلوبايت
const result = debounce(fn, 300);
// الأفضل: استخدام استيراد وحدات ES6
import { debounce } from 'lodash-es'; // قابل لإزالة الكود الميت
// إعداد Webpack لإزالة الكود الميت
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true
}
};
نصيحة: استخدم محللات الحزم مثل webpack-bundle-analyzer لتصور الاعتماديات التي تضخم حزمتك.
تقسيم الكود
قسّم الكود إلى أجزاء أصغر يتم تحميلها عند الطلب:
// تقسيم الكود حسب المسارات (مثال React)
import React, { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>جاري التحميل...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// الاستيراد الديناميكي (JavaScript عادي)
document.querySelector('#btn').addEventListener('click', async () => {
const module = await import('./heavy-feature.js');
module.initialize();
});
import React, { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>جاري التحميل...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// الاستيراد الديناميكي (JavaScript عادي)
document.querySelector('#btn').addEventListener('click', async () => {
const module = await import('./heavy-feature.js');
module.initialize();
});
التحميل الكسول
أجّل تحميل JavaScript غير الحرج حتى الحاجة إليه:
// Intersection Observer للتحميل الكسول
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
import('./chart-library.js').then(module => {
module.renderChart(entry.target);
});
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.chart-container').forEach(el => {
observer.observe(el);
});
// التحميل الكسول للسكريبتات مع السمات
const script = document.createElement('script');
script.src = 'analytics.js';
script.async = true;
script.defer = true;
document.body.appendChild(script);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
import('./chart-library.js').then(module => {
module.renderChart(entry.target);
});
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.chart-container').forEach(el => {
observer.observe(el);
});
// التحميل الكسول للسكريبتات مع السمات
const script = document.createElement('script');
script.src = 'analytics.js';
script.async = true;
script.defer = true;
document.body.appendChild(script);
تحذير: لا تحمّل المحتوى الحرج الموجود أعلى الصفحة أو الميزات المطلوبة للتفاعلية الأولية بشكل كسول. يمكن أن يضر هذا بتجربة المستخدم ومقاييس Core Web Vitals.
Web Workers (خيوط العمل الخلفية)
انقل العمليات الحسابية الثقيلة إلى خيوط الخلفية:
// worker.js
self.addEventListener('message', (e) => {
const data = e.data;
// عمليات حسابية ثقيلة
const result = processLargeDataset(data);
self.postMessage(result);
});
function processLargeDataset(data) {
// حسابات معقدة قد تحجب واجهة المستخدم
return data.map(item => expensiveOperation(item));
}
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeDataset);
worker.addEventListener('message', (e) => {
const result = e.data;
updateUI(result);
});
// التنظيف عند الانتهاء
worker.terminate();
self.addEventListener('message', (e) => {
const data = e.data;
// عمليات حسابية ثقيلة
const result = processLargeDataset(data);
self.postMessage(result);
});
function processLargeDataset(data) {
// حسابات معقدة قد تحجب واجهة المستخدم
return data.map(item => expensiveOperation(item));
}
// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeDataset);
worker.addEventListener('message', (e) => {
const result = e.data;
updateUI(result);
});
// التنظيف عند الانتهاء
worker.terminate();
requestAnimationFrame
حسّن الرسوم المتحركة والتحديثات المرئية:
// سيء: التحديثات خارج إطار الرسوم المتحركة
function moveElement() {
element.style.left = position + 'px';
position += 5;
setTimeout(moveElement, 16); // ~60fps لكنه غير دقيق
}
// جيد: استخدام requestAnimationFrame
function moveElement() {
element.style.left = position + 'px';
position += 5;
if (position < targetPosition) {
requestAnimationFrame(moveElement);
}
}
requestAnimationFrame(moveElement);
// تجميع قراءات وكتابات DOM
function optimizedLayout() {
// تجميع القراءات
const height1 = el1.offsetHeight;
const height2 = el2.offsetHeight;
// تجميع الكتابات
requestAnimationFrame(() => {
el1.style.height = height2 + 'px';
el2.style.height = height1 + 'px';
});
}
function moveElement() {
element.style.left = position + 'px';
position += 5;
setTimeout(moveElement, 16); // ~60fps لكنه غير دقيق
}
// جيد: استخدام requestAnimationFrame
function moveElement() {
element.style.left = position + 'px';
position += 5;
if (position < targetPosition) {
requestAnimationFrame(moveElement);
}
}
requestAnimationFrame(moveElement);
// تجميع قراءات وكتابات DOM
function optimizedLayout() {
// تجميع القراءات
const height1 = el1.offsetHeight;
const height2 = el2.offsetHeight;
// تجميع الكتابات
requestAnimationFrame(() => {
el1.style.height = height2 + 'px';
el2.style.height = height1 + 'px';
});
}
منع تسريبات الذاكرة
حدد ومنع أنماط تسريب الذاكرة الشائعة:
// تسريب: مستمعي الأحداث لم يتم إزالتهم
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
// نسيت إزالة المستمع - تسريب!
}
}
// مصلح: تنظيف المستمعين
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
document.removeEventListener('click', this.handleClick);
}
}
// تسريب: المؤقتات لم يتم مسحها
const intervalId = setInterval(() => {
updateData();
}, 1000);
// المكون يُزال لكن الفاصل الزمني يستمر - تسريب!
// مصلح: مسح المؤقتات
const intervalId = setInterval(() => updateData(), 1000);
window.addEventListener('beforeunload', () => {
clearInterval(intervalId);
});
// تسريب: عناصر DOM منفصلة
let cache = [];
function addElement() {
const div = document.createElement('div');
cache.push(div); // يحتفظ بالمرجع حتى بعد الإزالة
document.body.appendChild(div);
}
// مصلح: مسح المراجع
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
// لا تخزن مراجع لعناصر DOM
}
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
// نسيت إزالة المستمع - تسريب!
}
}
// مصلح: تنظيف المستمعين
class Component {
constructor() {
this.handleClick = () => console.log('clicked');
document.addEventListener('click', this.handleClick);
}
destroy() {
document.removeEventListener('click', this.handleClick);
}
}
// تسريب: المؤقتات لم يتم مسحها
const intervalId = setInterval(() => {
updateData();
}, 1000);
// المكون يُزال لكن الفاصل الزمني يستمر - تسريب!
// مصلح: مسح المؤقتات
const intervalId = setInterval(() => updateData(), 1000);
window.addEventListener('beforeunload', () => {
clearInterval(intervalId);
});
// تسريب: عناصر DOM منفصلة
let cache = [];
function addElement() {
const div = document.createElement('div');
cache.push(div); // يحتفظ بالمرجع حتى بعد الإزالة
document.body.appendChild(div);
}
// مصلح: مسح المراجع
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
// لا تخزن مراجع لعناصر DOM
}
نصيحة: استخدم Chrome DevTools Memory Profiler لاكتشاف تسريبات الذاكرة. التقط لقطات للذاكرة قبل وبعد الإجراءات لتحديد الكائنات التي لا يتم جمعها كنفايات.
مراقبة الأداء
// قياس وقت تنفيذ السكريبت
const startTime = performance.now();
heavyFunction();
const endTime = performance.now();
console.log(`استغرق التنفيذ ${endTime - startTime}ms`);
// مراقبة المهام الطويلة (أكثر من 50 ملي ثانية)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('تم اكتشاف مهمة طويلة:', {
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['longtask'] });
// User Timing API
performance.mark('feature-start');
await loadFeature();
performance.mark('feature-end');
performance.measure('feature', 'feature-start', 'feature-end');
const measure = performance.getEntriesByName('feature')[0];
console.log(`تحميل الميزة: ${measure.duration}ms`);
const startTime = performance.now();
heavyFunction();
const endTime = performance.now();
console.log(`استغرق التنفيذ ${endTime - startTime}ms`);
// مراقبة المهام الطويلة (أكثر من 50 ملي ثانية)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('تم اكتشاف مهمة طويلة:', {
duration: entry.duration,
startTime: entry.startTime
});
}
});
observer.observe({ entryTypes: ['longtask'] });
// User Timing API
performance.mark('feature-start');
await loadFeature();
performance.mark('feature-end');
performance.measure('feature', 'feature-start', 'feature-end');
const measure = performance.getEntriesByName('feature')[0];
console.log(`تحميل الميزة: ${measure.duration}ms`);
تمرين: حلل حزمة JavaScript لتطبيقك باستخدام webpack-bundle-analyzer. حدد الاعتماديات الثلاثة الأكبر وابحث عن بدائل قابلة لإزالة الكود الميت. نفّذ تقسيم الكود لمسارين على الأقل وقِس التأثير على وقت التحميل الأولي باستخدام Lighthouse.