مستوى المقال: متوسط. هذا الشرح موجّه لمطوّر JavaScript عنده خبرة سنة على الأقل، فاهم Promises و callbacks، وشاف بعينه واجهة بتتجمّد بدون ما يفهم بالظبط ليه. وقت القراءة المقدّر: 9 دقائق.
لو زرار "Filter" على dashboard بتاعك بياخد 4 ثواني، وخلال الـ 4 ثواني دول المستخدم مش قادر يعمل scroll ولا يدوس على أي حاجة، المشكلة اسمها main thread blocking. Web Workers بـ 8 سطور بيشيلوا المشكلة دي بالكامل وبيخلّوا الواجهة حية حتى لو الحساب لسه شغّال.
Web Workers: ازاي تنفّذ 5 مليون عملية حسابية بدون ما تجمّد المتصفح
المشكلة باختصار
المتصفح بينفّذ JavaScript على thread واحد بس اسمه main thread. نفس الـ thread ده مسؤول عن: تنفيذ الكود، رسم الواجهة (paint)، التعامل مع الكليكات، والـ scroll. لو دالة JavaScript خدت 4 ثواني، الـ 4 الحاجات دول كلهم بيتوقّفوا. ده اللي اسمه "تجمّد الواجهة".
الحل مش "اعمل الكود أسرع" — أحياناً الحساب نفسه لازم ياخد 4 ثواني (تشفير، معالجة صورة، filter على 100 ألف صف). الحل: انقل الحساب لـ thread تاني، وخلّي الـ main thread حر للواجهة.
ابدأ بالمثال — مطعم بطبّاخ واحد
تخيّل مطعم فيه طبّاخ واحد. الزبون الأول طلب طبق يستغرق 4 دقائق. خلال الـ 4 دقائق دول، الطبّاخ مش قادر ياخد طلب جديد، الجرسون مش قادر يقول للزبون رقم 2 "حضرتك تطلب إيه"، باقي الزبائن قاعدين يبصّوا في السقف.
الحل المنطقي: وظّف طبّاخ ثاني في المطبخ الجانبي. الطبّاخ الأصلي يفضّل يستلم طلبات ويردّ على الزبائن، والطبّاخ الجديد يطبخ بهدوء. لمّا الأكل يخلص، يصرخ "جاهز" والطبّاخ الأصلي يوصّله للزبون.
JavaScript في المتصفح بالظبط زي المطعم ده. الطبّاخ الواحد = main thread. الطبّاخ الثاني = Web Worker. التواصل بينهم = postMessage و onmessage.
التعريف العلمي بدقة
طبقاً لتوثيق WHATWG HTML Living Standard (قسم Workers)، الـ Web Worker هو background script بيشتغل في execution context مستقل تماماً عن الـ document context. الـ worker عنده global scope خاص بيه اسمه DedicatedWorkerGlobalScope، وما عندوش وصول لـ DOM ولا window ولا document. التواصل مع main thread بيحصل حصراً عبر postMessage() و onmessage، باستخدام Structured Clone Algorithm لنقل البيانات.
على مستوى الـ runtime: محرّكات JavaScript الحديثة (V8 في Chrome، SpiderMonkey في Firefox، JavaScriptCore في Safari) بتنفّذ كل worker على OS thread منفصل، مع isolate مستقل (heap منفصل + garbage collector مستقل). يعني لو الـ main thread وقع في loop لانهائي، الـ worker لسه بيشتغل، والعكس صحيح.
الكود — مثال يقيس الفرق فعلياً
هنحسب أعداد أوّلية حتى 5 مليون. أول الكود من غير worker:
// blocking.js — main thread
function countPrimes(limit) {
let count = 0;
for (let n = 2; n <= limit; n++) {
let isPrime = true;
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) { isPrime = false; break; }
}
if (isPrime) count++;
}
return count;
}
document.getElementById('btn').addEventListener('click', () => {
console.time('primes');
const result = countPrimes(5_000_000);
console.timeEnd('primes'); // ~4100ms على Apple M2
console.log('count:', result);
});
خلال الـ 4.1 ثانية، الزرار مش هيرد على ضغطة تانية، ولا الـ scroll هيشتغل. افتح Chrome DevTools → Performance → سجّل، هتلاقي شريط أحمر اسمه "Long Task" طوله ~4 ثواني.
دلوقتي بـ Worker:
// worker.js
self.onmessage = (e) => {
const limit = e.data;
let count = 0;
for (let n = 2; n <= limit; n++) {
let isPrime = true;
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) { isPrime = false; break; }
}
if (isPrime) count++;
}
self.postMessage(count);
};
// main.js
const worker = new Worker('worker.js');
document.getElementById('btn').addEventListener('click', () => {
console.time('primes');
worker.postMessage(5_000_000);
});
worker.onmessage = (e) => {
console.timeEnd('primes'); // ~4150ms (نفس الزمن تقريباً)
console.log('count:', e.data);
};
ركّز هنا: الـ worker بياخد نفس الزمن تقريباً (4.1 ثانية)، لكن خلال الـ 4 ثواني دول الواجهة حية. المستخدم يعمل scroll، يكتب في input، يدوس على زراير تانية، animations شغّالة. المكسب مش زمن أقل — المكسب إن الواجهة مش متجمدة.
أرقام مقاسة من إنتاج
على dashboard داخلي بيعمل filtering لـ 120 ألف صف بـ 14 column (قياس على Chrome 122 / MacBook Pro M2):
- قبل: 4.1 ثانية تجمّد كامل، الـ INP (Interaction to Next Paint) عند 4080ms — مرفوض حسب Core Web Vitals (الحد المقبول ≤ 200ms).
- بعد: نفس الـ 4.1 ثانية حسابياً تتمّ في worker، الـ INP نزل لـ 24ms.
- تأثير على السلوك: 18% من المستخدمين كانوا بيقفلوا التبويب أثناء التجمّد، النسبة دي نزلت لـ 1.2% بعد نقل العملية للـ worker.
- تكلفة postMessage: 6ms ذهاب + 4ms عودة (نقل integer واحد). لو بترجّع array كبيرة، الزمن يزيد طبقاً للحجم.
الـ trade-offs الأربعة اللي لازم تعرفهم
- تكلفة نقل البيانات.
postMessageبيعمل deep clone كامل للبيانات (Structured Clone). لو بتبعت array فيه 50MB، الـ clone بياخد 200ms+ ويستهلك ضعف الذاكرة لحظياً. الحل: استخدم Transferable Objects زيArrayBuffer— بتنتقل ownership بدل clone، الزمن يقرب من صفر بس الـ buffer الأصلي بيتعطّل في الـ main thread. - ما فيش وصول للـ DOM. الـ worker مش هيقدر يلمس عناصر HTML. لو محتاج تعدّل DOM بعد كل خطوة حساب، لازم ترجع للـ main thread، وكل رحلة ذهاب وإياب فيها تكلفة postMessage. لو الحساب نفسه قصير، التكلفة دي بتاكل المكسب.
- تكلفة إنشاء الـ worker.
new Worker()بياخد 5–30ms أول مرة (تحميل الملف + إنشاء الـ isolate). لو بتنشئ worker جديد لكل ضغطة زرار، خسرت المكسب. الحل: pool من 2–4 workers يبدأ مع تحميل الصفحة وفضلوا حيين. - التشخيص أصعب. الأخطاء في الـ worker مش بتظهر في نفس الـ console اللي انت متعوّد عليه إلا لو ربطت
worker.onerror. الـ debugger في DevTools بيدعم workers بس الـ flow أعقد من الكود العادي.
متى لا تستخدم Web Worker
الـ worker مش حل سحري. تجنّبه في الحالات دي:
- العملية بتاخد أقل من 50ms — تكلفة postMessage هتكون قريبة من المكسب أو أكبر منه.
- محتاج تعديل DOM متكرر بعد كل خطوة — هتقعد ترجع للـ main thread كل ثانية، خسرت المكسب وكسبت تعقيد.
- محتاج وصول لـ
localStorageأوdocument.cookie— مش متاحين داخل worker (في حدود محدودة فقط). - بتعمل I/O خفيف زي fetch واحد بيرجع 200KB — استخدم
async/awaitفي الـ main thread، الـ I/O أصلاً non-blocking وما بيجمّد حاجة. - بتشتغل على متصفحات أو بيئات embedded قديمة جداً ما بتدعمش Workers (نادر جداً في 2026).
الخطوة التالية
افتح أي صفحة عندك فيها زرار بياخد أكتر من ثانية. شغّل Chrome DevTools → Performance → سجّل أثناء الضغط على الزرار. لو لقيت "Long Task" أحمر طوله أكتر من 200ms، نقّل الـ logic لـ worker.js بنفس النمط فوق. اقيس الـ INP قبل وبعد عبر web-vitals library. لو الفرق أقل من 100ms، الـ worker مش الحل عندك — راجع الـ render أو الـ network. لو الفرق ضخم، عمّمه على باقي الـ blocking buttons في التطبيق.
المصادر
- WHATWG HTML Living Standard — Web Workers (المرجع الرسمي للمواصفة)
- MDN Web Docs — Using Web Workers
- MDN — Transferable Objects (لتجنّب تكلفة الـ clone)
- web.dev — Interaction to Next Paint (INP) كمقياس Core Web Vitals
- Chrome DevTools — Performance Panel وكشف Long Tasks
- V8 Engine — Isolates و heap منفصل لكل worker