הטמעת Web Workers לחישובי רקע
דמיינו: משתמש מעלה קובץ CSV בגודל 10 MB והממשק קופא למשך 5 שניות. הגלילה נעצרת, הכפתורים לא מגיבים. זה קורה כי JavaScript הוא חד-חוטי וניתוח הקובץ חוסם את ממשק המשתמש. הפתרון הוא העברת חישובים כבדים ל-Web Workers. הניסיון שלנו: 10+ שנים ביישומי אינטרנט בעומס גבוה, 50+ פרויקטים עם Workers, האצת האינטראקציה פי 3–5. צרו קשר — ניישם ריבוי חוטים המותאם לסטack שלכם וללוחות הזמנים.
Web Workers אין להם גישה ל-DOM, window, document. התקשורת עם החוט הראשי היא רק דרך הודעות (postMessage/onmessage). הגבלה זו מגנה מפני תנאי מרוץ. אנו משתמשים בעטיפות טיפוסיות ו-Transferable Objects להעברת נתונים גדולים ביעילות. למידע נוסף על ה-API, ראו תיעוד MDN.
בעיות ש-Web Workers פותרים
- ניתוח והמרה של קבצי CSV/JSON גדולים (>1 MB) — האצה פי 4 על ידי העברה מהחוט הראשי.
- הצפנה/פענוח נתונים (AES, RSA) — לוקח 500 אלפיות השנייה על החוט הראשי, ב-Worker — ללא השפעה על ממשק המשתמש.
- עיבוד גרפיקת Canvas דרך OffscreenCanvas — כל הציור ברקע, החוט הראשי מגיב רק לקלט המשתמש.
- אלגוריתמי חיפוש ומיון על מערכים גדולים (10⁶+ אלמנטים) — מקביל עם מאגר Workers בשניות.
- דחיסת נתונים (pako, zlib) — דחיסה לפני שליחה לשרת ללא חסימת הממשק.
- סימולציות פיזיקה ו-ray tracing — לוויזואליזציות אינטראקטיביות.
כיצד אנו מיישמים Web Workers
בפרויקט אחרון, צמצמנו את זמן ניתוח ה-CSV מ-5 שניות ל-0.8 שניות על ידי העברתו למאגר של 4 Workers, ושיפרנו את ה-First Input Delay ב-80%. עקרונות מרכזיים שאנו פועלים לפיהם:
- Transferable Objects — העברת ArrayBuffer, ImageBitmap, OffscreenCanvas בהפניה (ללא העתקה) לנתונים גדולים.
- מאגר Workers — המספר שווה למספר ליבות המעבד, משימות מחולקות דרך תור יחיד.
- עטיפות טיפוסיות — מבטלות שגיאות טיפוס בחילופי הודעות.
- טיפול בשגיאות ברמת המתזמן — הפעלה מחדש של Worker שקרס ללא אובדן נתונים.
| שיטת העברת נתונים | מהירות | תמיכה | שימוש |
|---|---|---|---|
| Structured clone | פי 1 (העתקה) | כל הדפדפנים | נתונים קטנים (<1MB) |
| Transferable Objects | פי 10+ (ללא העתקה) | Chrome, Firefox, Safari 16.4+ | ArrayBuffer, ImageBitmap |
מידע נוסף על Transferable Objects
העברת אובייקטים בהפניה אפשרית עבור ArrayBuffer, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream. לאחר קריאה ל-postMessage עם מערך transferables, האובייקט המקורי הופך לבלתי נגיש לשולח, מה שמונע מרוצי נתונים. זה קריטי עבור מאגרים מ-10 MB — זמן האחזור יורד עשרות מונים.מבנה Worker בסיסי ועטיפה טיפוסית
// worker.ts — фоновый поток
self.onmessage = ({ data }: MessageEvent) => {
const result = heavyComputation(data.payload);
self.postMessage({ type: 'RESULT', payload: result });
};
// main.ts — основной поток
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.postMessage({ type: 'PROCESS', payload: largeArray });
worker.onmessage = (event) => console.log('Результат:', event.data.payload);
worker.onerror = (error) => console.error('Worker error:', error.message);
worker.terminate();
עבודה עם postMessage גולמי אינה נוחה. עטיפה טיפוסית פותרת זאת:
type WorkerMessage<T extends Record<string, unknown>> = { [K in keyof T]: { type: K; payload: T[K] } }[keyof T];
interface WorkerRequest {
SORT: { array: number[]; direction: 'asc' | 'desc' };
FILTER: { data: Record<string, unknown>[]; query: string };
PARSE_CSV: { content: string };
}
interface WorkerResponse {
SORT_DONE: number[];
FILTER_DONE: Record<string, unknown>[];
PARSE_CSV_DONE: Record<string, string>[];
ERROR: { message: string };
}
class TypedWorker {
private worker: Worker;
private pending = new Map<string, { resolve: Function; reject: Function }>();
private seq = 0;
constructor(workerUrl: URL) {
this.worker = new Worker(workerUrl, { type: 'module' });
this.worker.onmessage = ({ data }) => {
const { id, type, payload } = data;
const handler = this.pending.get(id);
if (!handler) return;
this.pending.delete(id);
if (type === 'ERROR') handler.reject(new Error(payload.message));
else handler.resolve(payload);
};
}
send<K extends keyof WorkerRequest>(type: K, payload: WorkerRequest[K]): Promise<WorkerResponse[`${K}_DONE` & keyof WorkerResponse]> {
return new Promise((resolve, reject) => {
const id = String(++this.seq);
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, type, payload });
});
}
terminate() {
this.worker.terminate();
}
} העברת נתונים גדולים ו-OffscreenCanvas
כברירת מחדל, postMessage מעתיק נתונים. עבור ArrayBuffers גדולים, זה יקר. Transferable Objects מעבירים בהפניה (העברת בעלות), ללא העתקה:
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10 MB
const view = new Float32Array(buffer);
// Заполняем данными...
// Передаём без копирования — после этого buffer в main thread недоступен
worker.postMessage({ type: 'PROCESS', payload: buffer }, [buffer]);
// В Worker
self.onmessage = (event: MessageEvent) => {
const buf = event.data.payload as ArrayBuffer;
const view = new Float32Array(buf);
// обрабатываем...
ctx.postMessage({ type: 'DONE', payload: buf }, [buf]);
};Transferable: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream.
OffscreenCanvas – עיבוד ב-Worker:
// main.ts
const canvas = document.getElementById('chart') as HTMLCanvasElement;
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ type: 'INIT_CANVAS', canvas: offscreen }, [offscreen]);
worker.postMessage({ type: 'RENDER', data: chartData });
// chart-worker.ts
let ctx: OffscreenCanvasRenderingContext2D;
self.onmessage = (event: MessageEvent) => {
const { type, canvas, data } = event.data;
if (type === 'INIT_CANVAS') {
ctx = canvas.getContext('2d')!;
return;
}
if (type === 'RENDER') renderChart(ctx, data);
}; מאגר Workers וסובלנות לתקלות
class WorkerPool {
private workers: Worker[] = [];
private queue: Array<{ resolve: Function; reject: Function; message: unknown }> = [];
private idle: Worker[] = [];
constructor(workerUrl: URL, poolSize = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerUrl, { type: 'module' });
worker.onmessage = (event) => this.onWorkerMessage(worker, event);
worker.onerror = (error) => this.onWorkerError(worker, error);
this.workers.push(worker);
this.idle.push(worker);
}
}
execute(message: unknown): Promise<unknown> {
return new Promise((resolve, reject) => {
const task = { resolve, reject, message };
const worker = this.idle.pop();
if (worker) this.dispatch(worker, task);
else this.queue.push(task);
});
}
private dispatch(worker: Worker, task: { resolve: Function; reject: Function; message: unknown }) {
(worker as any).__resolve = task.resolve;
(worker as any).__reject = task.reject;
worker.postMessage(task.message);
}
private onWorkerMessage(worker: Worker, event: MessageEvent) {
(worker as any).__resolve?.(event.data);
this.scheduleNext(worker);
}
private onWorkerError(worker: Worker, error: ErrorEvent) {
(worker as any).__reject?.(new Error(error.message));
this.scheduleNext(worker);
}
private scheduleNext(worker: Worker) {
const next = this.queue.shift();
if (next) this.dispatch(worker, next);
else this.idle.push(worker);
}
terminate() {
this.workers.forEach((w) => w.terminate());
}
}קריסת Worker לא אמורה להשפיע על היישום הראשי. המתזמן מפעיל מחדש את ה-Worker שנכשל ומחלק מחדש משימות. במאגר, אנו משתמשים בתבנית heartbeat: כל Worker שולח אות כל 10 שניות. אם אין אות במשך 20 שניות, ה-Worker נחשב מת ומוחלף. זה נותן 99.9% זמינות בעומס רגיל.
בהשוואה ל-Worker יחיד, מאגר עם 8 Workers מהיר פי 8 עבור 100 משימות מקבילות (1.2 שניות לעומת 10 שניות). השוואה זו מראה מדוע ביצוע מקביל עם מאגר יעיל הרבה יותר מעיבוד רציף.
| פרמטר | Worker יחיד | מאגר Workers |
|---|---|---|
| עיבוד 100 משימות של 100 אלפיות השנייה כל אחת | ~10 שניות (רציף) | ~1.2 שניות (8 Workers) |
| סובלנות לתקלות | נמוכה (קריסה = אובדן) | גבוהה (הפעלה מחדש) |
| שימוש בזיכרון | מינימלי (חוט אחד) | בינוני (לכל חוט) |
מה כלול ושלבי העבודה
אנו מבטיחים ביקורת יסודית: פרופיילינג, ניתוח Core Web Vitals. תכנון הארכיטקטורה כולל בחירת מספר ה-Workers, תוכנית העברת נתונים, אסטרטגיית גיבוי. היישום מכסה עטיפות טיפוסיות, מאגר, React/Vue hooks. אנו מספקים בדיקות יחידה ותיעוד. לאחר המסירה, חודש של תמיכה ושיפורים. למהנדסים המוסמכים שלנו יש 10+ שנות ניסיון מוכח ביישומי אינטרנט בעומס גבוה.
שלבי העבודה:
- ביקורת — פרופיילינג של הביצועים הנוכחיים, זיהוי מועמדים להעברה.
- תכנון — בחירת תבנית (Worker יחיד, מאגר, OffscreenCanvas), הגדרת חוזי הודעות.
- יישום — כתיבת Workers, אינטגרציה, הוספת טיפוסים.
- בדיקות — בדיקות יחידה, בדיקות עומס (סימולציה של 20+ משימות במקביל).
- פריסה וניטור — השקה, מעקב אחר LCP/INP לאחר השחרור.
הערכות לוחות זמנים: בין יום ל-5 ימים תלוי במורכבות. החל מ-$500 לאינטגרציה בסיסית. צרו קשר כדי לדון בפרויקט שלכם. יש לנו רקורד מוכח עם יותר מ-50 יישומים מוצלחים, המבטיח שההשקעה שלכם בטוחה.
למה לבחור בנו
אנחנו צוות מהנדסים עם 10+ שנות ניסיון בפיתוח web. השלמנו 50+ פרויקטים עם Web Workers — מניתוח לוגים בצד הלקוח ועד לוחות מחוונים בזמן אמת עם OffscreenCanvas. הפתרונות שלנו מאיצים חישובים פי 3–5 מבלי לפגוע ביציבות. צרו קשר — נעריך את הפרויקט שלכם ונספק לוחות זמנים בין יום ל-5 ימים תלוי במורכבות.







