عمال الويب والأداء
فهم عنق الزجاجة في الخيط الرئيسي
تعمل JavaScript في المتصفح على خيط واحد يسمى الخيط الرئيسي. هذا الخيط الواحد مسؤول عن كل ما تراه وتتفاعل معه: تحليل HTML وحساب أنماط CSS وتنفيذ JavaScript ومعالجة مدخلات المستخدم مثل النقرات والتمرير ورسم البكسلات على الشاشة وتشغيل جمع القمامة. عندما تستغرق أي من هذه المهام وقتا طويلا، يجب أن ينتظر كل شيء آخر. والنتيجة هي ما يختبره المستخدمون كتأخير أو تقطع أو واجهة مجمدة تماما.
تخيل ما يحدث عند تشغيل عملية مكلفة حسابيا مثل فرز مليون سجل أو حساب تسلسلات رياضية معقدة أو معالجة صورة كبيرة بكسل ببكسل. أثناء تشغيل كود JavaScript هذا، لا يستطيع المتصفح الاستجابة لنقرات الأزرار أو أحداث التمرير أو حتى إعادة رسم الشاشة. يرى المستخدم صفحة مجمدة، وإذا استغرقت المهمة أكثر من بضع ثوان، قد يعرض المتصفح مربع حوار تحذيري بأن الصفحة لا تستجيب.
هذا القيد أحادي الخيط مقصود بالتصميم. نموذج DOM ليس آمنا للخيوط المتعددة، مما يعني أنه إذا حاولت خيوط متعددة تعديل DOM في نفس الوقت، ستكون النتائج غير متوقعة وفوضوية. ومع ذلك، يخلق هذا التصميم تحديا خطيرا: كيف تشغل حسابات ثقيلة دون حظر واجهة المستخدم؟ الجواب هو عمال الويب (Web Workers).
ما هم عمال الويب؟
عمال الويب هم واجهة برمجة تطبيقات المتصفح التي تسمح لك بتشغيل كود JavaScript في خيط خلفي منفصل، مستقل تماما عن الخيط الرئيسي. يمتلك عامل الويب سياق تنفيذ خاص به وحلقة أحداث خاصة ومساحة ذاكرة خاصة. الكود الذي يعمل داخل العامل لا يحظر الخيط الرئيسي، مما يعني أن واجهة المستخدم تظل مستجيبة حتى أثناء تشغيل الحسابات الثقيلة في الخلفية.
فكر في الأمر بهذه الطريقة: الخيط الرئيسي مثل طاهٍ في مطعم يجب عليه أخذ الطلبات وطهي الطعام وخدمة الطاولات وتنظيف الأطباق. إذا استغرق طبق معقد 30 دقيقة للتحضير، تتوقف كل مهمة أخرى. عمال الويب مثل توظيف طاقم مطبخ إضافي -- يمكن تحضير الطبق المعقد بواسطة طاه مخصص بينما يستمر الطاهي الرئيسي في خدمة العملاء الآخرين.
window أو كائن document أو أي واجهات برمجة تطبيقات متعلقة بالواجهة. غرضهم الوحيد هو الحساب ومعالجة البيانات، ويتواصلون مع الخيط الرئيسي حصريا من خلال نظام المراسلة.إنشاء أول عامل ويب
لإنشاء عامل ويب، تحتاج إلى ملفين: النص البرمجي الرئيسي وملف نص برمجي منفصل للعامل. يحتوي نص العامل البرمجي على الكود الذي سيعمل في خيط الخلفية. تقوم بإنشاء عامل عن طريق تمرير مسار نص العامل البرمجي إلى مُنشئ Worker.
مثال: النص البرمجي الرئيسي (main.js)
// إنشاء عامل جديد بتحديد ملف نص العامل البرمجي
const worker = new Worker('worker.js');
// إرسال البيانات إلى العامل
worker.postMessage({ task: 'fibonacci', number: 45 });
// الاستماع للرسائل (النتائج) من العامل
worker.onmessage = function(event) {
console.log('النتيجة من العامل:', event.data);
document.getElementById('result').textContent = event.data.result;
};
// معالجة الأخطاء التي تحدث داخل العامل
worker.onerror = function(error) {
console.error('خطأ العامل:', error.message);
console.error('في الملف:', error.filename, 'في السطر:', error.lineno);
};
مثال: نص العامل البرمجي (worker.js)
// الاستماع للرسائل من الخيط الرئيسي
self.onmessage = function(event) {
const data = event.data;
if (data.task === 'fibonacci') {
const result = calculateFibonacci(data.number);
// إرسال النتيجة إلى الخيط الرئيسي
self.postMessage({ result: result, number: data.number });
}
};
function calculateFibonacci(n) {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}
يقبل مُنشئ Worker سلسلة URL تشير إلى ملف نص العامل البرمجي. داخل العامل، النطاق العام هو self (وليس window). يستمع العامل للرسائل باستخدام self.onmessage ويرسل البيانات مرة أخرى باستخدام self.postMessage. على جانب الخيط الرئيسي، تستخدم worker.postMessage لإرسال البيانات وworker.onmessage لاستقبال النتائج.
نمط التواصل postMessage و onmessage
يحدث التواصل بين الخيط الرئيسي والعامل حصريا من خلال نمط تمرير الرسائل. لا يمكنك مشاركة المتغيرات مباشرة بين الخيطين. بدلا من ذلك، ترسل البيانات باستخدام postMessage() وتستقبلها من خلال معالج حدث onmessage. يتم استنساخ البيانات هيكليا، مما يعني أن المتصفح ينشئ نسخة عميقة من البيانات. الأصل والنسخة مستقلان تماما -- تعديل أحدهما لا يؤثر على الآخر.
مثال: التواصل ثنائي الاتجاه
// main.js
const worker = new Worker('data-processor.js');
// إرسال مصفوفة من الأرقام للمعالجة
worker.postMessage({
action: 'sort',
data: [45, 12, 89, 3, 67, 23, 91, 8, 56, 34]
});
// إرسال مهمة أخرى أثناء معالجة الأولى
worker.postMessage({
action: 'statistics',
data: [100, 200, 150, 300, 250, 175, 225]
});
worker.onmessage = function(event) {
const response = event.data;
if (response.action === 'sort') {
console.log('مرتبة:', response.result);
} else if (response.action === 'statistics') {
console.log('المتوسط:', response.result.mean);
console.log('الوسيط:', response.result.median);
}
};
// data-processor.js (العامل)
self.onmessage = function(event) {
const { action, data } = event.data;
if (action === 'sort') {
const sorted = data.slice().sort(function(a, b) { return a - b; });
self.postMessage({ action: 'sort', result: sorted });
} else if (action === 'statistics') {
const sum = data.reduce(function(acc, val) { return acc + val; }, 0);
const mean = sum / data.length;
const sortedData = data.slice().sort(function(a, b) { return a - b; });
const mid = Math.floor(sortedData.length / 2);
const median = sortedData.length % 2 !== 0
? sortedData[mid]
: (sortedData[mid - 1] + sortedData[mid]) / 2;
self.postMessage({ action: 'statistics', result: { mean: mean, median: median } });
}
};
action أو type في رسائلك حتى يتمكن كل من العامل والخيط الرئيسي من تحديد نوع المهمة أو الاستجابة التي يتعاملون معها. يتوسع هذا النمط جيدا عندما يتعامل عاملك مع أنواع متعددة من العمليات.الكائنات القابلة للنقل للأداء العالي
تقوم عملية الاستنساخ الهيكلي بنسخ البيانات عند إرسال الرسائل. للبيانات الصغيرة هذا سريع، لكن للبيانات الكبيرة مثل كائنات ArrayBuffer الكبيرة (مثل بيانات الصور أو مخازن الصوت المؤقتة)، يمكن أن يكون النسخ مكلفا وبطيئا. الكائنات القابلة للنقل تحل هذه المشكلة عن طريق نقل ملكية الذاكرة من خيط إلى آخر بدلا من نسخها. النقل فوري تقريبا بغض النظر عن حجم البيانات، لكن بعد النقل لا يمكن للخيط الأصلي الوصول إلى تلك البيانات بعد الآن.
مثال: نقل ArrayBuffer
// main.js -- نقل مخزن مؤقت كبير إلى العامل
const largeBuffer = new ArrayBuffer(1024 * 1024 * 50); // مخزن 50 ميجابايت
const uint8View = new Uint8Array(largeBuffer);
// ملء ببيانات عينة
for (let i = 0; i < uint8View.length; i++) {
uint8View[i] = i % 256;
}
console.log('حجم المخزن قبل النقل:', largeBuffer.byteLength); // 52428800
// نقل المخزن -- الوسيط الثاني هو قائمة الكائنات القابلة للنقل
worker.postMessage({ buffer: largeBuffer }, [largeBuffer]);
console.log('حجم المخزن بعد النقل:', largeBuffer.byteLength); // 0 (تم النقل!)
// worker.js -- استقبال المخزن المنقول
self.onmessage = function(event) {
const buffer = event.data.buffer;
const view = new Uint8Array(buffer);
console.log('تم استقبال مخزن بحجم:', buffer.byteLength);
// معالجة البيانات...
// نقلها مرة أخرى عند الانتهاء
self.postMessage({ buffer: buffer }, [buffer]);
};
ArrayBuffer، يصبح المتغير الأصلي مخزنا بطول صفر ولم يعد قابلا للاستخدام. محاولة القراءة أو الكتابة إليه لن تنتج خطأ لكن البيانات ستكون فارغة. خطط لتدفق بياناتك بعناية عند استخدام الكائنات القابلة للنقل.قيود العامل: ما لا يستطيع العمال فعله
يعمل عمال الويب في بيئة مقيدة. فهم ما لا يمكن للعمال الوصول إليه مهم بقدر معرفة ما يمكنهم فعله. إليك قائمة شاملة بالقيود:
- لا وصول لـ DOM -- لا يستطيع العمال قراءة أو تعديل
documentأو إنشاء عناصر أو تغيير بنية الصفحة. جميع تحديثات DOM يجب أن تحدث على الخيط الرئيسي. - لا كائن window -- الكائن العام
windowغير متاح. استخدمselfبدلا من ذلك للإشارة إلى النطاق العام للعامل. - لا وصول للأب -- لا يستطيع العمال الوصول مباشرة إلى المتغيرات أو الدوال المعرفة في نص الخيط الرئيسي.
- واجهات برمجة محدودة -- يمكن للعمال استخدام
fetchوXMLHttpRequestوsetTimeoutوsetIntervalوIndexedDBوWebSocketsوimportScripts، لكن ليسlocalStorageأوsessionStorageأو أي واجهات عرض. - سياسة نفس المصدر -- يجب تقديم ملف نص العامل من نفس مصدر الصفحة أو تحميله كعنوان URL blob.
مثال: ما يعمل وما لا يعمل في العامل
// داخل worker.js
// هذه تعمل في العامل:
self.fetch('https://api.example.com/data')
.then(function(response) { return response.json(); })
.then(function(data) { self.postMessage(data); });
setTimeout(function() {
self.postMessage('رسالة متأخرة من العامل');
}, 1000);
const url = new URL('https://example.com/path');
const now = Date.now();
const id = crypto.randomUUID();
// هذه تفشل في العامل (ستطرح ReferenceError):
// document.getElementById('test'); // لا DOM
// window.alert('مرحبا'); // لا window
// localStorage.setItem('key', 'v'); // لا localStorage
SharedWorker: عامل واحد لعدة تبويبات
Worker العادي مرتبط بالصفحة التي أنشأته. إذا فتحت عدة تبويبات لنفس الموقع، ينشئ كل تبويب عامله الخاص. SharedWorker يحل هذا بالسماح لعدة تبويبات أو إطارات iframe أو نوافذ من نفس المصدر بمشاركة مثيل عامل واحد. هذا مفيد لمهام مثل الحفاظ على اتصال WebSocket مشترك أو مزامنة الحالة عبر التبويبات أو إدارة ذاكرة تخزين مؤقت مشتركة.
مثال: تواصل SharedWorker
// main.js -- الاتصال بـ SharedWorker
const sharedWorker = new SharedWorker('shared-worker.js');
// SharedWorker يستخدم منفذا للتواصل
sharedWorker.port.start();
sharedWorker.port.postMessage({ type: 'join', tabId: Date.now() });
sharedWorker.port.onmessage = function(event) {
console.log('رسالة من العامل المشترك:', event.data);
};
// shared-worker.js
const connectedPorts = [];
self.onconnect = function(event) {
const port = event.ports[0];
connectedPorts.push(port);
port.onmessage = function(msgEvent) {
const data = msgEvent.data;
if (data.type === 'join') {
// إخطار جميع التبويبات المتصلة بالاتصال الجديد
connectedPorts.forEach(function(p) {
p.postMessage({
type: 'update',
connectedTabs: connectedPorts.length
});
});
}
};
port.start();
};
MessagePort بدلا من postMessage مباشرة على العامل نفسه. كل سياق متصل (تبويب، إطار iframe) يستقبل منفذه الخاص. يجب عليك استدعاء port.start() لبدء استقبال الرسائل.حالات استخدام حقيقية للعمال
يتألق عمال الويب في السيناريوهات التي تتضمن حسابات ثقيلة أو معالجة بيانات. إليك حالات استخدام عملية حيث يحسن العمال تجربة المستخدم بشكل كبير:
الحساب الثقيل: توليد الأعداد الأولية
مثال: حساب الأعداد الأولية في عامل
// prime-worker.js
self.onmessage = function(event) {
const limit = event.data.limit;
const primes = [];
for (let num = 2; num <= limit; num++) {
let isPrime = true;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(num);
}
// الإبلاغ عن التقدم كل 10000 رقم
if (num % 10000 === 0) {
self.postMessage({
type: 'progress',
checked: num,
total: limit,
found: primes.length
});
}
}
self.postMessage({ type: 'complete', primes: primes });
};
// main.js
const primeWorker = new Worker('prime-worker.js');
primeWorker.postMessage({ limit: 1000000 });
primeWorker.onmessage = function(event) {
if (event.data.type === 'progress') {
const percent = Math.round((event.data.checked / event.data.total) * 100);
document.getElementById('progress').textContent = percent + '% مكتمل';
} else if (event.data.type === 'complete') {
document.getElementById('result').textContent =
'تم العثور على ' + event.data.primes.length + ' عدد أولي';
}
};
معالجة البيانات: تحليل CSV
مثال: تحليل بيانات CSV كبيرة في عامل
// csv-worker.js
self.onmessage = function(event) {
const csvText = event.data.csv;
const lines = csvText.split('\n');
const headers = lines[0].split(',').map(function(h) { return h.trim(); });
const records = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '') continue;
const values = lines[i].split(',');
const record = {};
headers.forEach(function(header, index) {
record[header] = values[index] ? values[index].trim() : '';
});
records.push(record);
}
self.postMessage({
headers: headers,
records: records,
totalRows: records.length
});
};
معالجة الصور: تطبيق المرشحات
مثال: مرشح التدرج الرمادي في عامل
// image-worker.js
self.onmessage = function(event) {
const imageData = event.data.imageData;
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const red = pixels[i];
const green = pixels[i + 1];
const blue = pixels[i + 2];
// صيغة السطوع للتدرج الرمادي الإدراكي
const gray = Math.round(0.299 * red + 0.587 * green + 0.114 * blue);
pixels[i] = gray; // أحمر
pixels[i + 1] = gray; // أخضر
pixels[i + 2] = gray; // أزرق
// pixels[i + 3] هو ألفا، يبقى دون تغيير
}
self.postMessage({ imageData: imageData }, [imageData.data.buffer]);
};
// main.js -- استخراج بيانات الصورة وإرسالها للعامل
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const imageWorker = new Worker('image-worker.js');
imageWorker.postMessage(
{ imageData: imageData },
[imageData.data.buffer]
);
imageWorker.onmessage = function(event) {
ctx.putImageData(event.data.imageData, 0, 0);
};
قياس الأداء باستخدام performance.now()
قبل أن تتمكن من تحسين الأداء، تحتاج إلى قياسه بدقة. تُرجع طريقة performance.now() طابعا زمنيا عالي الدقة بالملي ثانية، بدقة تصل إلى الميكروثانية. على عكس Date.now()، تستخدم ساعة أحادية الاتجاه لا تتأثر بتغييرات ساعة النظام وتبدأ من وقت التنقل في الصفحة بدلا من حقبة Unix.
مثال: التوقيت الدقيق باستخدام performance.now()
// قياس المدة التي تستغرقها دالة للتنفيذ
const startTime = performance.now();
// محاكاة عملية مكلفة
let sum = 0;
for (let i = 0; i < 10000000; i++) {
sum += Math.sqrt(i);
}
const endTime = performance.now();
const duration = endTime - startTime;
console.log('استغرقت العملية ' + duration.toFixed(2) + ' ملي ثانية');
console.log('النتيجة: ' + sum);
// مقارنة نهجين
function measureFunction(fn, label) {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(label + ': ' + (end - start).toFixed(2) + 'ms');
return result;
}
measureFunction(function() {
return Array.from({ length: 100000 }, function(_, i) { return i * 2; });
}, 'Array.from');
measureFunction(function() {
const arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(i * 2);
}
return arr;
}, 'الدفع اليدوي');
واجهة الأداء و PerformanceObserver
توفر واجهة الأداء مجموعة أدوات شاملة لقياس جوانب مختلفة من أداء الصفحة. يمكنك إنشاء علامات وقياسات أداء مخصصة ومراقبة المهام الطويلة وتحليل توقيتات تحميل التنقل والموارد.
مثال: علامات وقياسات الأداء
// إنشاء علامات عند نقاط محددة في الكود
performance.mark('data-fetch-start');
fetch('https://api.example.com/users')
.then(function(response) { return response.json(); })
.then(function(data) {
performance.mark('data-fetch-end');
// إنشاء قياس بين علامتين
performance.measure('data-fetch-duration', 'data-fetch-start', 'data-fetch-end');
// استرجاع القياس
const measures = performance.getEntriesByName('data-fetch-duration');
console.log('استغرق الجلب:', measures[0].duration.toFixed(2), 'ملي ثانية');
performance.mark('render-start');
renderUserList(data);
performance.mark('render-end');
performance.measure('render-duration', 'render-start', 'render-end');
const renderMeasure = performance.getEntriesByName('render-duration');
console.log('استغرق العرض:', renderMeasure[0].duration.toFixed(2), 'ملي ثانية');
});
// استخدام PerformanceObserver لمراقبة المهام الطويلة (مهام > 50 ملي ثانية)
const observer = new PerformanceObserver(function(list) {
list.getEntries().forEach(function(entry) {
console.warn('تم اكتشاف مهمة طويلة!');
console.warn('المدة:', entry.duration.toFixed(2), 'ملي ثانية');
console.warn('وقت البدء:', entry.startTime.toFixed(2), 'ملي ثانية');
});
});
observer.observe({ entryTypes: ['longtask'] });
استخدام console.time للقياسات السريعة
لقياسات الأداء السريعة وغير الرسمية أثناء التطوير، يوفر console.time() وconsole.timeEnd() اختصارا مريحا. يسجلان تلقائيا الوقت المنقضي مع تسمية في وحدة التحكم.
مثال: console.time للقياسات السريعة
// توقيت بسيط
console.time('فرز-المصفوفة');
const bigArray = Array.from({ length: 500000 }, function() {
return Math.random();
});
bigArray.sort();
console.timeEnd('فرز-المصفوفة'); // يسجل: فرز-المصفوفة: 142.35ms
// مؤقتات متداخلة بتسميات مختلفة
console.time('العملية-الكاملة');
console.time('الخطوة-1-التوليد');
const data = Array.from({ length: 100000 }, function(_, i) {
return { id: i, value: Math.random() * 1000 };
});
console.timeEnd('الخطوة-1-التوليد');
console.time('الخطوة-2-التصفية');
const filtered = data.filter(function(item) {
return item.value > 500;
});
console.timeEnd('الخطوة-2-التصفية');
console.time('الخطوة-3-التحويل');
const transformed = filtered.map(function(item) {
return { id: item.id, doubled: item.value * 2 };
});
console.timeEnd('الخطوة-3-التحويل');
console.timeEnd('العملية-الكاملة');
// استخدام console.timeLog للتحقق من القيم المتوسطة
console.time('عملية-طويلة');
for (let i = 0; i < 5; i++) {
// محاكاة عمل
const arr = new Array(100000).fill(0).map(Math.random);
arr.sort();
console.timeLog('عملية-طويلة', 'اكتملت التكرار ' + i);
}
console.timeEnd('عملية-طويلة');
المهام الطويلة والتقطع
يهدف المتصفح إلى عرض الإطارات بمعدل 60 إطارا في الثانية، مما يعطي كل إطار ميزانية تقارب 16.67 ملي ثانية. خلال ذلك الوقت، يجب على المتصفح تشغيل JavaScript وحساب الأنماط وتنفيذ التخطيط والرسم وتركيب الطبقات. عندما تستغرق مهمة JavaScript أكثر من 50 ملي ثانية، تُصنف كـ مهمة طويلة، وستتسبب بشكل شبه مؤكد في تقطع -- تلعثم مرئي أو تجمد في الرسوم المتحركة أو التمرير أو تفاعلات المستخدم.
الأسباب الشائعة للمهام الطويلة تشمل: معالجة مجموعات بيانات كبيرة بشكل متزامن، والتلاعب المعقد بـ DOM في حلقة، والتعبيرات المنتظمة المكلفة على سلاسل كبيرة، والقراءات المتزامنة للتخطيط متبوعة بالكتابة (ارتجاج التخطيط)، وتحليل أو تسلسل كائنات JSON كبيرة.
مثال: تقسيم المهام الطويلة بالإنتاجية
// سيء: معالجة جميع العناصر دفعة واحدة تحظر الخيط الرئيسي
function processAllAtOnce(items) {
items.forEach(function(item) {
expensiveCalculation(item);
});
}
// جيد: إعطاء الأولوية للخيط الرئيسي بين الأجزاء
function processInChunks(items, chunkSize) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, items.length);
while (index < end) {
expensiveCalculation(items[index]);
index++;
}
if (index < items.length) {
// إعطاء الأولوية للمتصفح ثم متابعة الجزء التالي
setTimeout(processChunk, 0);
} else {
console.log('تمت معالجة جميع العناصر!');
}
}
processChunk();
}
// أفضل: استخدام requestIdleCallback للعمل غير العاجل
function processWhenIdle(items) {
let index = 0;
function processNext(deadline) {
// معالجة العناصر أثناء وجود وقت خمول متبقي
while (index < items.length && deadline.timeRemaining() > 5) {
expensiveCalculation(items[index]);
index++;
}
if (index < items.length) {
requestIdleCallback(processNext);
} else {
console.log('تمت معالجة جميع العناصر أثناء وقت الخمول!');
}
}
requestIdleCallback(processNext);
}
setTimeout(fn, 0) يعمل فورا. إنه يضع الاستدعاء في قائمة الانتظار ليعمل بعد تفريغ مكدس الاستدعاء الحالي وبعد أن يحصل المتصفح على فرصة لمعالجة الأحداث المعلقة والعرض. التأخير الفعلي عادة 4 ملي ثانية أو أكثر.استراتيجيات تقسيم الكود
تقسيم الكود هو ممارسة تقسيم حزمة JavaScript إلى أجزاء أصغر يتم تحميلها عند الطلب بدلا من تحميلها جميعا مرة واحدة. عندما يزور المستخدم صفحتك، يجب أن يحمل فقط الكود الضروري للعرض الحالي. يتم تحميل الكود الإضافي في الخلفية أو عندما ينتقل المستخدم إلى قسم يتطلبه.
مثال: الاستيراد الديناميكي لتقسيم الكود
// بدلا من استيراد كل شيء في أعلى الملف:
// import { heavyChartLibrary } from './charts.js';
// تحميل مكتبة الرسوم البيانية فقط عند نقر المستخدم على الزر
document.getElementById('show-chart-btn').addEventListener('click', function() {
// عرض مؤشر التحميل
document.getElementById('chart-container').textContent = 'جاري تحميل الرسم البياني...';
// الاستيراد الديناميكي يُرجع وعدا
import('./charts.js')
.then(function(module) {
// تم تحميل الوحدة الآن؛ استخدمها
module.renderChart('chart-container', chartData);
})
.catch(function(error) {
console.error('فشل تحميل وحدة الرسم البياني:', error);
document.getElementById('chart-container').textContent =
'خطأ في تحميل الرسم البياني. يرجى المحاولة مرة أخرى.';
});
});
// مثال تقسيم الكود القائم على المسارات
function navigateTo(route) {
if (route === '/dashboard') {
import('./pages/dashboard.js').then(function(module) {
module.init();
});
} else if (route === '/settings') {
import('./pages/settings.js').then(function(module) {
module.init();
});
} else if (route === '/reports') {
import('./pages/reports.js').then(function(module) {
module.init();
});
}
}
التحميل الكسول
التحميل الكسول هو استراتيجية حيث يتم تحميل الموارد فقط عند الحاجة إليها بدلا من تحميلها مسبقا. ينطبق هذا على الصور والنصوص البرمجية والمكونات وحتى البيانات. الشكل الأكثر شيوعا للتحميل الكسول في المتصفحات الحديثة هو سمة loading="lazy" الأصلية للصور وإطارات iframe، لكن يمكنك تنفيذ التحميل الكسول لأي مورد باستخدام واجهة Intersection Observer.
مثال: التحميل الكسول للصور باستخدام Intersection Observer
// HTML: <img data-src="photo.jpg" alt="وصف" class="lazy-img">
function lazyLoadImages() {
const lazyImages = document.querySelectorAll('.lazy-img');
const imageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const img = entry.target;
// استبدال data-src بـ src الفعلي لبدء التحميل
img.src = img.dataset.src;
img.addEventListener('load', function() {
img.classList.add('loaded');
});
img.addEventListener('error', function() {
img.src = 'placeholder.jpg';
img.alt = 'فشل تحميل الصورة';
});
// التوقف عن مراقبة هذه الصورة لأنها تم تحميلها
observer.unobserve(img);
}
});
}, {
rootMargin: '100px 0px', // بدء التحميل قبل 100 بكسل من الظهور
threshold: 0.01
});
lazyImages.forEach(function(img) {
imageObserver.observe(img);
});
}
// تهيئة التحميل الكسول عندما يكون DOM جاهزا
document.addEventListener('DOMContentLoaded', lazyLoadImages);
rootMargin مثل '100px 0px' على Intersection Observer الخاص بك حتى تبدأ الصور بالتحميل قبل أن تنزلق إلى العرض مباشرة. هذا يخلق تجربة أكثر سلاسة لأن الصورة ستكون محملة بالكامل غالبا بحلول الوقت الذي يمرر فيه المستخدم إليها.مفهوم التمرير الافتراضي
عندما يكون لديك قائمة من آلاف أو عشرات الآلاف من العناصر، عرضها جميعا في DOM مرة واحدة مكلف للغاية. التمرير الافتراضي (يسمى أيضا العرض النوافذي) يحل هذا بعرض العناصر المرئية حاليا في إطار العرض فقط، بالإضافة إلى منطقة عازلة صغيرة فوق وتحت. مع تمرير المستخدم، تُزال العناصر التي تغادر إطار العرض من DOM، وتُضاف عناصر جديدة تدخل إطار العرض. هذا يبقي عدد عقد DOM صغيرا وثابتا بغض النظر عن الحجم الإجمالي للقائمة.
مثال: تنفيذ أساسي للتمرير الافتراضي
function createVirtualList(container, items, itemHeight) {
const visibleCount = Math.ceil(container.clientHeight / itemHeight);
const bufferCount = 5; // عناصر إضافية فوق وتحت
const totalHeight = items.length * itemHeight;
// إنشاء فاصل طويل للحفاظ على ارتفاع التمرير
const spacer = document.createElement('div');
spacer.style.height = totalHeight + 'px';
spacer.style.position = 'relative';
container.appendChild(spacer);
// حاوية المحتوى للعناصر المرئية
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.width = '100%';
spacer.appendChild(content);
function renderVisibleItems() {
const scrollTop = container.scrollTop;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);
const endIndex = Math.min(
items.length,
startIndex + visibleCount + bufferCount * 2
);
// تحديد موضع حاوية المحتوى
content.style.top = (startIndex * itemHeight) + 'px';
// مسح وإعادة عرض العناصر المرئية فقط
content.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const itemElement = document.createElement('div');
itemElement.style.height = itemHeight + 'px';
itemElement.style.boxSizing = 'border-box';
itemElement.textContent = items[i];
content.appendChild(itemElement);
}
}
container.addEventListener('scroll', renderVisibleItems);
renderVisibleItems(); // العرض الأولي
}
// الاستخدام: عرض 100,000 عنصر بكفاءة
const container = document.getElementById('list-container');
const items = Array.from({ length: 100000 }, function(_, i) {
return 'العنصر #' + (i + 1);
});
createVirtualList(container, items, 40);
إنهاء العمال
عندما لا تحتاج إلى عامل بعد الآن، من المهم إنهاؤه لتحرير موارد النظام. يمكنك إنهاء عامل من الخيط الرئيسي باستخدام worker.terminate()، أو يمكن للعامل إغلاق نفسه باستخدام self.close(). لا يمكن إعادة تشغيل العمال المنتهيين -- يجب إنشاء مثيل Worker جديد إذا كنت بحاجة لواحد مرة أخرى.
مثال: إدارة دورة حياة العامل
// إنشاء عامل لمهمة محددة
const worker = new Worker('task-worker.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = function(event) {
if (event.data.type === 'complete') {
console.log('انتهت المهمة، إنهاء العامل');
worker.terminate(); // تحرير الموارد
}
};
// تعيين مهلة للإنهاء إذا استغرقت المهمة وقتا طويلا
const timeoutId = setTimeout(function() {
console.warn('انتهت مهلة العامل، إنهاء');
worker.terminate();
}, 30000);
// داخل worker.js -- إنهاء ذاتي بعد المهمة
self.onmessage = function(event) {
const result = processData(event.data);
self.postMessage({ type: 'complete', result: result });
self.close(); // العامل ينهي نفسه
};
تمرين عملي
ابنِ تطبيق ويب يوضح عمال الويب وتحسين الأداء. أنشئ صفحة بزرين: واحد يحسب مجموع جميع الأعداد الأولية حتى 5 ملايين على الخيط الرئيسي، وآخر يقوم بنفس الحساب باستخدام عامل ويب. أضف رسما متحركا CSS دوارا على الصفحة. عند النقر على زر الخيط الرئيسي، لاحظ كيف يتجمد الرسم المتحرك. عند النقر على زر العامل، يجب أن يظل الرسم المتحرك سلسا. اعرض وقت الحساب باستخدام performance.now() لكلا النهجين. أضف شريط تقدم يتحدث في الوقت الفعلي أثناء معالجة العامل لأجزاء من الأرقام. ثم نفذ قائمة تمرير افتراضي أسفل الأزرار تعرض 50,000 عنصر، كل منها يظهر رقم عنصر وعينة لون عشوائية. أخيرا، أضف زرا ثالثا يقوم بالتحميل الكسول لوحدة JavaScript ثقيلة باستخدام import() الديناميكي ويسجل وقت التحميل في وحدة التحكم.