لو dashboard بتاعك بيتجمّد 4 ثواني كل ما المستخدم يرفع ملف CSV حجمه 80MB، المشكلة مش في حجم الملف ولا في سرعة اللاب. JavaScript بيشتغل على thread واحد، وأي عملية parsing بتقفل الـ UI كله — مفيش scroll، مفيش click، حتى الـ animation بيقف.
Web Workers بـ 30 سطر بتنقل العملية الثقيلة دي لـ thread موازي، فالـ UI يفضل سلس 60fps والمستخدم يقدر يلغي العملية لو غيّر رأيه.
Web Workers: التوازي اللي JavaScript مكنش بيعرفه
المشكلة باختصار
JavaScript في المتصفح بيشتغل على main thread واحد. الـ thread ده مسؤول عن 4 حاجات في نفس الوقت: تنفيذ الكود، رسم الصفحة، الاستجابة للـ events (click, scroll, keyboard)، وتشغيل الـ animations.
لو دالة JavaScript بتاخد 800ms في الحساب، الـ 4 حاجات دي بتقف 800ms. المستخدم بيشوف الصفحة "متجمّدة"، وفي Chrome بيظهرله "Page Unresponsive" بعد 5 ثواني.
مثال للمبتدئ: مطعم الكاشير الواحد
تخيّل مطعم فيه كاشير واحد بيعمل كل حاجة: ياخد الطلب من العميل، يحضّر الأكل، يعمل الفاتورة، يستلم الفلوس، يرد الباقي. لو جه عميل عايز كباب — والكباب محتاج 12 دقيقة على الفحم — الكاشير هيقف 12 دقيقة قدام الفرن، والـ 18 عميل اللي ورا ميلاقوش حد يطلب منه. المطعم كله بيتجمّد لحد ما الكباب يخلص.
الحل المنطقي: تجيب طبّاخ في المطبخ. الكاشير يكتب الطلب على ورقة، يبعتها للمطبخ من الباب الجانبي، ويرجع يخدم العملاء. الطبّاخ يحضّر، وأول ما الأكل يبقى جاهز، يقول للكاشير "خد الطلب رقم 47". الكاشير ساعتها بس بيوقف ثانية يستلم.
الكاشير = main thread. الطبّاخ = Web Worker. ورقة الطلب = postMessage.
Web Worker علميًا
طبقًا لـ HTML Living Standard (قسم Web Workers)، الـ Web Worker هو script بيشتغل في خلفية المتصفح في execution context منفصل تمامًا عن الـ main thread. الفصل ده بمعنى الكلمة:
- مفيش shared memory افتراضيًا (إلا لو استخدمت SharedArrayBuffer صراحة).
- الـ Worker مش شايف الـ DOM ولا window ولا document.
- التواصل بين الـ main thread والـ Worker بيتم عبر
postMessage()اللي بيـ serialize البيانات (Structured Clone Algorithm).
الفصل ده هو السبب اللي بيخلّي Web Workers آمنة: مفيش race conditions على متغير مشترك زي ما بيحصل في threads بـ C++ أو Java.
5 خطوات لتشغيل Web Worker (بكود فعلي)
هنبني مثال واقعي: parsing لملف CSV 82MB فيه بيانات مبيعات (1.4 مليون صف).
الخطوة 1: اعمل ملف الـ Worker
// csv-worker.js
self.onmessage = (event) => {
const { fileText } = event.data;
const rows = fileText.split('\n');
const result = [];
for (let i = 1; i < rows.length; i++) {
const cols = rows[i].split(',');
if (cols.length < 3) continue;
result.push({
date: cols[0],
product: cols[1],
amount: parseFloat(cols[2]),
});
if (i % 50000 === 0) {
self.postMessage({ type: 'progress', percent: (i / rows.length) * 100 });
}
}
self.postMessage({ type: 'done', result });
};الخطوة 2: شغّل الـ Worker من الـ main thread
// main.js (Vite, ESM)
const worker = new Worker(
new URL('./csv-worker.js', import.meta.url),
{ type: 'module' }
);
document.querySelector('#file').addEventListener('change', async (e) => {
const file = e.target.files[0];
const text = await file.text();
worker.postMessage({ fileText: text });
});
worker.onmessage = (event) => {
const { type, percent, result } = event.data;
if (type === 'progress') {
progressBar.value = percent;
} else if (type === 'done') {
renderTable(result);
worker.terminate();
}
};الخطوة 3: اقيس الفرق
على ملف CSV 82MB، Chrome 130، MacBook Air M2:
- بدون Worker: UI مجمّد 4,180ms، الـ scroll مش شغّال، 0 fps خلال الـ parsing.
- بـ Worker: UI شغّال 60fps طول الوقت، main thread واقف بس 18ms (بتاع الـ postMessage)، الـ progress bar شغّال طبيعي.
Trade-offs لازم تعرفها قبل ما تستخدمها
Web Workers مش حل سحري. كل توصية ليها ثمنها:
- Serialization مش مجاني. postMessage بينسخ البيانات بـ Structured Clone. لو بتبعت array فيها مليون object، النسخ نفسه بياخد 200-400ms. الـ Transferable Objects (مثل ArrayBuffer) بتنقل الملكية بدون نسخ، بس مش كل البيانات بتدعمها.
- مفيش DOM. الـ Worker مش هيقدر يعدّل HTML أو يقرا قيمة من input. لازم ترجّع البيانات للـ main thread وهو اللي يلمس الـ DOM.
- تكلفة تشغيل. فتح Worker بياخد 5-15ms ثبات. لو هتعمل عملية أصلًا بتاخد 30ms، الـ Worker بيضيع وقت أكتر مما يوفّر.
- Debugging أصعب. الـ stack trace في الـ Worker منفصل. لازم تفتح Sources tab في DevTools وتختار الـ Worker يدويًا.
أفضل حالات استخدام Web Workers
- Parsing ملفات كبيرة: CSV, JSON, XML أكتر من 5MB.
- Image processing: filters, resize, compression قبل الرفع.
- Cryptography: hashing, encryption لكميات بيانات كبيرة.
- تحليل بيانات: aggregations, sorting على آلاف الصفوف.
- WebAssembly heavy compute: physics simulations, video encoding.
متى لا تستخدم Web Workers
الافتراض إن العملية ثقيلة فعلًا. لو الكود بتاعك:
- بياخد أقل من 50ms على main thread → سيب الكود مكانه. الـ Worker overhead أكبر من المكسب.
- محتاج DOM access مباشر → Worker مش هينفع. فكّر في
requestIdleCallbackأو تقسيم العمل لـ chunks بـsetTimeout. - بيتم مرة واحدة في عمر الـ session → التعقيد الإضافي مش مبرّر.
- على بيانات حجمها أقل من 1MB والوقت أقل من 100ms → الفرق مش هيتحس بصريًا.
الـ trade-off الحقيقي: Web Worker بيشيل العبء عن main thread، بس بيضيف تعقيد في الـ build (Vite, webpack محتاجة config) وفي الـ debugging. لو dashboard بسيط بيشتغل تمام، متعقّدش الكود من غير سبب.
الخطوة التالية
افتح المشروع بتاعك، دوّر على أبطأ عملية في الـ frontend (الأغلب parsing أو filtering على مصفوفة كبيرة)، وقيس زمنها بـ performance.now(). لو طلع أكتر من 100ms، انسخ الـ template اللي فوق وحوّلها لـ Worker. لو الزمن أقل من 50ms، اتركها — التوازي مش حل لكل مشكلة.