Web Workers بالعربي: خلّي الحسابات الثقيلة بعيد عن الواجهة
مستوى القارئ: متوسط
هتخرج من المقال بطريقة عملية تمنع حساب JavaScript تقيل من تجميد الواجهة، بدل ما المستخدم يضغط زرار ويستنى الصفحة تفوق.
المشكلة باختصار
اللي بيحصل فعلاً إن JavaScript في المتصفح بيشتغل غالبًا على الـ main thread. نفس الخيط ده مسؤول عن الكليك، الرسم، تحديث الـ DOM، وتنفيذ الكود. لو حطيت عليه حساب بياخد 800ms، الواجهة هتبقى متجمدة طول المدة دي.
سيناريو واقعي: عندك لوحة أسعار بتستورد 50 ألف صف من CSV، وبعدها تحسب margin وtax وdiscount لكل صف. على لابتوب قوي ممكن الحساب ياخد 250ms. على موبايل متوسط ممكن يوصل 900ms. الرقم ده كفاية يخلي المستخدم يحس إن الصفحة علقت.
الفكرة بمثال بسيط
ركز في المثال ده: عندك موظف استقبال واحد. لو خليته يرد على العملاء ويحسب كشف حساب 50 ألف صف في نفس الوقت، العملاء هيقفوا. الأفضل إنك تبعت كشف الحساب لمحاسب في غرفة تانية، ولما يخلص يرجّع النتيجة. الاستقبال يفضل يرد على الناس.
ده بالظبط دور Web Worker. الواجهة تبعت بيانات للـ worker باستخدام postMessage. العامل يحسب بعيد عن الـ main thread. وبعدها يرجع النتيجة برسالة تانية. المكسب إن الكليك والـ typing يفضلوا شغالين. الخسارة إن فيه تكلفة نقل بيانات بين الخيطين.
مثال تنفيذي قابل للنسخ
الافتراض إن عندك حساب CPU-bound واضح: فلترة أو تجميع أو parsing كبير. المثال التالي يحسب إجمالي أسعار عدد كبير من المنتجات خارج الواجهة.
// main.js
const worker = new Worker("/pricing-worker.js", { type: "module" });
button.addEventListener("click", () => {
const started = performance.now();
worker.postMessage({ items: window.bigPriceList });
worker.onmessage = (event) => {
totalBox.textContent = event.data.total.toFixed(2);
timeBox.textContent = `${Math.round(performance.now() - started)}ms`;
};
});
// pricing-worker.js
self.onmessage = (event) => {
const total = event.data.items.reduce((sum, item) => {
const afterDiscount = item.price * (1 - item.discount);
return sum + afterDiscount * 1.14;
}, 0);
self.postMessage({ total });
};لو شغلت نفس الحساب على الـ main thread، ممكن تشوف 840ms من UI blocking على جهاز متوسط. بعد النقل للـ worker، الحساب نفسه قد يظل قريبًا من 840ms، لكن الواجهة لا تتجمد بنفس الشكل. في قياس عملي بسيط، وقت الرسائل والتحديث النهائي ممكن يبقى حوالي 120ms بدل حجب كامل للواجهة.
ما الذي يتحسن فعلاً
أفضل طريقة تفكر بها في Web Workers: هي لا تجعل الحساب نفسه سحريًا أسرع دائمًا. هي تنقل الحمل بعيدًا عن الخيط الذي يحتاجه المستخدم للتفاعل. ده مهم جدًا مع INP، لأن تقليل الضغط على الـ main thread يساعد الصفحة ترد أسرع على تفاعل المستخدم.
الـ trade-off هنا واضح. بتكسب واجهة أكثر استجابة، وبتخسر بساطة الكود. بدل دالة مباشرة ترجع قيمة، عندك رسائل، async flow، واحتمال تحتاج تربط كل request بـ id لو عندك أكثر من عملية شغالة.
- استخدمه مع parsing كبير، ضغط بيانات، image processing، أو حسابات جداول كبيرة.
- لا تحاول تحديث DOM من داخل worker. الـ DOM يفضل على main thread.
- لو البيانات كبيرة جدًا، فكّر في Transferable Objects مثل
ArrayBufferبدل نسخ كائنات ضخمة.
متى لا تستخدم هذه الطريقة
لا تستخدم Web Worker لو العملية خفيفة تحت 50ms. تكلفة الفصل والرسائل ممكن تبقى أكبر من المكسب. لا تستخدمه أيضًا لو الكود كله مربوط بـ DOM أو React state مباشرة. افصل الحساب النقي الأول، وبعدها انقله.
ولا تستخدمه كبديل لتنظيف الخوارزمية. لو عندك loop بـ O(N²) على 100 ألف عنصر، انقله للـ worker هيخلي الواجهة تستجيب، لكنه لن يحل استهلاك CPU. أفضل ترتيب: حسّن الخوارزمية، قِس، ثم انقل الجزء المتبقي للـ worker لو لسه بيحجب الواجهة.
مصادر اعتمد عليها
- MDN: Using Web Workers
- MDN: Worker.postMessage
- web.dev: Use web workers to run JavaScript off the main thread
الخطوة التالية
افتح أبطأ دالة عندك في الواجهة وقِسها بـ performance.now(). لو بتتجاوز 100ms وبلا تعامل مباشر مع DOM، انقلها إلى worker صغير بنفس نمط الكود فوق.