Worker Patterns
รูปแบบ request/response
หัวข้อที่มีชื่อว่า “รูปแบบ request/response”Worker ทำงานแบบ asynchronous และอาจจัดการงานหลายอย่างพร้อมกัน เพื่อจับคู่ response กับ request ที่ถูกต้อง ให้แนบ id ที่ไม่ซ้ำกันไปกับแต่ละข้อความ:
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.jsself.onmessage = (e) => { const { id, payload } = e.data; self.postMessage({ id, result: process(payload) });};RPC wrapper ขนาดเล็ก
หัวข้อที่มีชื่อว่า “RPC wrapper ขนาดเล็ก”การห่อ 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 ด้วย onerror
หัวข้อที่มีชื่อว่า “การจัดการ error ด้วย onerror”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”| ควรใช้ 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 โดยตรง) |
ตัวอย่างที่รันได้: request/response ด้วย message ID
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้: request/response ด้วย message ID”ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| 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เอง