ข้ามไปยังเนื้อหา

Worker Patterns

Worker ทำงานแบบ asynchronous และอาจจัดการงานหลายอย่างพร้อมกัน เพื่อจับคู่ response กับ request ที่ถูกต้อง ให้แนบ id ที่ไม่ซ้ำกันไปกับแต่ละข้อความ:

main.js
let nextId = 0;
const pending = {};
worker.onmessage = (e) => {
const { id, result } = e.data;
if (pending[id]) { pending[id](result); delete pending[id]; }
};
function ask(payload) {
return new Promise((resolve) => {
const id = ++nextId;
pending[id] = resolve;
worker.postMessage({ id, payload });
});
}
// worker.js
self.onmessage = (e) => {
const { id, payload } = e.data;
self.postMessage({ id, result: process(payload) });
};

การห่อ pattern ข้างต้นไว้ใน helper ขนาดเล็กทำให้การเรียก worker รู้สึกเหมือนการเรียก async function ทั่วไป

function createRpc(worker) {
let seq = 0;
const map = {};
worker.onmessage = (e) => {
const cb = map[e.data.id];
if (cb) { cb(e.data.result); delete map[e.data.id]; }
};
return {
call(method, args) {
return new Promise((res) => {
const id = ++seq;
map[id] = res;
worker.postMessage({ id, method, args });
});
}
};
}

error ที่ไม่ถูก catch ภายใน worker จะยิง worker.onerror ในเธรดหลัก — ไม่ใช่ window.onerror ควรแนบ handler เสมอ:

worker.onerror = (event) => {
console.error('Worker error:', event.message, 'at', event.filename, ':', event.lineno);
event.preventDefault(); // suppress browser console error
};

นอกจากนี้ยังสามารถแนบ self.onerror ภายใน worker เองเพื่อดัก async error ก่อนที่จะ bubble ขึ้นไปยังเธรดหลักได้

ควรใช้ workerไม่ควรใช้ worker
Loop ที่ใช้ CPU หนัก (crypto, image processing, compression)การอ่าน/เขียน DOM อย่างง่าย
การ parse JSON หรือ binary data ขนาดใหญ่งานสั้น < 5 ms
การประมวลผลข้อมูล real-time (audio, video)งานที่ต้องการเข้าถึง DOM โดยตรง
การรักษา UI ให้อยู่ที่ 60 fps ระหว่างงานหนักการ fetch async อย่างง่ายครั้งเดียว (ใช้ fetch โดยตรง)
Browser Storage
ตัวเลือกBenefitCost
RPC wrapper (message ID + Promise)เรียก worker ได้เหมือน async function ทั่วไป จัดการ concurrent request หลายตัวพร้อมกันได้อย่างเป็นระบบต้องเขียน boilerplate เพิ่ม (id counter, pending map) และเพิ่ม layer ของ abstraction ที่ debug ยากขึ้นเล็กน้อย
postMessage/onmessage แบบดิบเรียบง่าย เห็น flow ตรงไปตรงมา เหมาะกับ worker ที่มี message pattern ไม่ซับซ้อนถ้ามีหลาย request ค้างพร้อมกัน ต้องจับคู่ response เองด้วยมือ เสี่ยง race condition
แนบ worker.onerror handlerจับ error ที่ไม่ถูก catch ได้ ป้องกันความล้มเหลวแบบเงียบใน productionต้องเขียน handler เพิ่มทุก worker และออกแบบให้ error ไม่ทำให้ pending promise ค้างตลอดไป
  • ไม่แนบ worker.onerror — error ที่ไม่ถูก catch ภายใน worker จะไม่ปรากฏใน window.onerror เลย ถ้าไม่มี handler การ crash จะล้มเหลวแบบเงียบและตรวจจับได้ยากมากใน production
  • ปล่อยให้ pending/map โตขึ้นเรื่อยๆ โดยไม่ลบ entry — ถ้า request ไม่ได้รับ response กลับมา (เช่น worker crash) entry ใน map จะค้างอยู่ตลอดไปและกลายเป็น memory leak ควรมี timeout หรือ cleanup เมื่อ worker error
  • ใช้ worker สำหรับงานที่ไม่คุ้มกับ overhead — เช่น การอ่าน event คลิกปุ่ม หรือ fetch อย่างง่ายครั้งเดียว ต้นทุนของการตั้ง RPC wrapper และการสื่อสารข้ามเธรดสูงกว่าประโยชน์ที่ได้มาก

💡 ตัวอย่างจากของจริง

Figma — ใช้ pattern คล้าย RPC ที่มี message ID เพื่อจับคู่คำขอคำนวณ layout/rendering หลายรายการที่ยิงพร้อมกันไปยัง worker กับผลลัพธ์ที่ตอบกลับมาไม่เรียงลำดับ

Squoosh (เครื่องมือบีบอัดภาพของ Google) — ห่อการเรียก codec worker ด้วย wrapper แบบ Promise-based ทำให้โค้ดฝั่ง UI เรียกใช้ worker เหมือนเรียก async function ปกติ โดยไม่ต้องจัดการ postMessage/onmessage เอง

ทำไมจึงต้องใส่ `id` ที่ไม่ซ้ำกันในแต่ละ worker message?
error ที่ไม่ถูก catch ภายใน Web Worker จะยิงที่ไหน?
งานใดเป็น use case ที่เหมาะสมสำหรับ Web Worker?
ใน pattern RPC ขนาดเล็ก object `map` เก็บอะไร?