أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
البرمجة بالعربي

Web Workers في JavaScript للمتوسط: شغّل خوارزمية ثقيلة بدون ما تجمّد الواجهة 4 ثواني

📅 ٨ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Web Workers في JavaScript للمتوسط: شغّل خوارزمية ثقيلة بدون ما تجمّد الواجهة 4 ثواني

مستوى المقال: متوسط. هذا الشرح موجّه لمطوّر JavaScript عنده خبرة سنة على الأقل، فاهم Promises و callbacks، وشاف بعينه واجهة بتتجمّد بدون ما يفهم بالظبط ليه. وقت القراءة المقدّر: 9 دقائق.

لو زرار "Filter" على dashboard بتاعك بياخد 4 ثواني، وخلال الـ 4 ثواني دول المستخدم مش قادر يعمل scroll ولا يدوس على أي حاجة، المشكلة اسمها main thread blocking. Web Workers بـ 8 سطور بيشيلوا المشكلة دي بالكامل وبيخلّوا الواجهة حية حتى لو الحساب لسه شغّال.

دائرة معالج إلكترونية تمثّل التنفيذ المتوازي على عدة threads داخل المتصفح

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.

مطبخ مطعم فيه أكثر من طبّاخ يعملون في نفس الوقت، تشبيه لتشغيل main thread جنب worker thread

التعريف العلمي بدقة

طبقاً لتوثيق 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:

JavaScript
// 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:

JavaScript
// 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);
};
JavaScript
// 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 الأربعة اللي لازم تعرفهم

  1. تكلفة نقل البيانات. postMessage بيعمل deep clone كامل للبيانات (Structured Clone). لو بتبعت array فيه 50MB، الـ clone بياخد 200ms+ ويستهلك ضعف الذاكرة لحظياً. الحل: استخدم Transferable Objects زي ArrayBuffer — بتنتقل ownership بدل clone، الزمن يقرب من صفر بس الـ buffer الأصلي بيتعطّل في الـ main thread.
  2. ما فيش وصول للـ DOM. الـ worker مش هيقدر يلمس عناصر HTML. لو محتاج تعدّل DOM بعد كل خطوة حساب، لازم ترجع للـ main thread، وكل رحلة ذهاب وإياب فيها تكلفة postMessage. لو الحساب نفسه قصير، التكلفة دي بتاكل المكسب.
  3. تكلفة إنشاء الـ worker. new Worker() بياخد 5–30ms أول مرة (تحميل الملف + إنشاء الـ isolate). لو بتنشئ worker جديد لكل ضغطة زرار، خسرت المكسب. الحل: pool من 2–4 workers يبدأ مع تحميل الصفحة وفضلوا حيين.
  4. التشخيص أصعب. الأخطاء في الـ 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

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة