Web Workers في JavaScript: نقل الحسابات الثقيلة بره الـ main thread
المستوى: متوسط — يفترض إن عندك خبرة بـ JavaScript والـ DOM، ومش شرط تكون اشتغلت على threading قبل كده.
لو الواجهة بتعلّق لمدة ثانيتين كل ما المستخدم يضغط زر "تصدير CSV" أو "ولّد التقرير"، المشكلة مش إن الكود بطيء. المشكلة إنه شغّال على نفس الـ thread اللي بيرسم الواجهة. Web Workers هينقل الحسابات الثقيلة في خلفية موازية، والواجهة تفضل تستجيب بـ INP أقل من 200ms حتى وقت الحساب.
المشكلة باختصار
المتصفح بيدّيك thread واحد بس للـ JavaScript على كل tab: الـ main thread. الـ thread ده بيعمل أربع حاجات في نفس الوقت — تنفيذ الكود، رسم الـ DOM، الاستجابة لأحداث المستخدم، وتشغيل الـ animations. لو حساب واحد عدّى 50ms عليه، أي حاجة في الطابور بعده هتتأخر بنفس المقدار. المستخدم بيشوف ده على شكل ضغطات زرار مش بترد، scroll بيقطع، وحقول إدخال بتأكل الحروف. مقياس INP في Core Web Vitals بيقيس الظاهرة دي تحديدًا، والحد الأخضر هو 200ms.
المثال البسيط الأول: المطبخ والشيف
تخيّل مطعم فيه شيف واحد. لو طلب جالك "بيتزا تحتاج 20 دقيقة فرن"، الشيف هيقعد متابع الفرن طول المدة دي، ومحدش هيقدر يطلب حاجة تانية. المشكلة مش إن الشيف بطيء، المشكلة إنه واحد بس وعنده مهام تانية قاعدة في الطابور. الحل البديهي مش إنك تخلّيه يطبخ أسرع، الحل إنك تجيب عامل تاني معاه فرن منفصل، يستلم الطلب، والشيف يكمل خدمة باقي الزبائن.
الـ main thread هو الشيف. الـ Web Worker هو العامل التاني اللي معاه فرن خاص بيه. الفكرة دي بالظبط اسمها concurrency: مش بنخلّي الحساب أسرع، بنخلّي الواجهة تفضل قادرة ترد وهو شغّال.
التعريف العلمي للـ Web Worker
الـ Web Worker هو سياق تنفيذ JavaScript مستقل، بيشتغل في OS thread منفصل عن الـ main thread، ومعاه global scope خاص بيه. الـ worker معندوش وصول مباشر للـ DOM ولا للـ window ولا لـ document. التواصل بين الـ main thread والـ worker بيحصل بآلية message passing عبر الدالتين postMessage() و onmessage، مع نسخ البيانات افتراضيًا باستخدام structured clone، أو نقل ملكيتها بدون نسخ عبر transferable objects للأنواع زي ArrayBuffer.
مثال تنفيذي: عملية حساب ثقيلة على الكلاينت
هنفترض إن عندك صفحة لتحليل بيانات بتنفّذ حساب recursive ثقيل (فيبوناتشي 42 كنموذج بسيط لأي حساب CPU-bound). هنبني الكود مرتين، مرة على الـ main thread ومرة على worker، ونقيس INP بين الحالتين.
الكود قبل التحسين — كل حاجة على الـ main thread
// app.js
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
document.querySelector('#run').addEventListener('click', () => {
console.time('fib');
const result = fib(42);
console.timeEnd('fib');
document.querySelector('#out').textContent = result;
});