עכשיו קל יותר להעביר משימות כבדות לשרשורים ברקע באמצעות מודולים של JavaScript ב-Web Workers.
JavaScript הוא שפת תכנות עם תהליך יחיד (single-threaded), כלומר הוא יכול לבצע רק פעולה אחת בכל פעם. זה אינטואיטיבי ועובד טוב בהרבה מקרים באינטרנט, אבל יכול להיות בעייתי כשצריך לבצע משימות כבדות כמו עיבוד נתונים, ניתוח תחבירי, חישוב או ניתוח. ככל שמועברות באינטרנט אפליקציות מורכבות יותר, כך גובר הצורך בעיבוד מרובה-תהליכים.
בפלטפורמת האינטרנט, הפרימיטיב הראשי לשרשורים ולמקביליות הוא Web Workers API. Workers הם הפשטה קלה של שרשורים של מערכת ההפעלה שחושפים ממשק API להעברת הודעות לצורך תקשורת בין שרשורים. האפשרות הזו יכולה להיות שימושית מאוד כשמבצעים חישובים יקרים או כשמפעילים קבוצות נתונים גדולות, כי היא מאפשרת לשרשור הראשי לפעול בצורה חלקה בזמן שהפעולות היקרות מתבצעות בשרשור אחד או יותר ברקע.
דוגמה אופיינית לשימוש ב-worker, שבה סקריפט של worker מאזין להודעות מהשרשור הראשי ומגיב על ידי שליחת הודעות משלו:
page.js:
const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
console.log(e.data);
});
worker.postMessage('hello');
worker.js:
addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});
Web Worker API זמין ברוב הדפדפנים כבר יותר מעשר שנים. זה אומר שהם נתמכים היטב בדפדפנים ועברו אופטימיזציה, אבל גם שהם נוצרו הרבה לפני מודולים של JavaScript. מכיוון שלא הייתה מערכת מודולים כשה-workers תוכננו, ה-API לטעינת קוד ל-worker וליצירת סקריפטים נשאר דומה לגישות הטעינה של סקריפטים סינכרוניים שהיו נפוצות ב-2009.
היסטוריה: עובדים קלאסיים
ה-Worker constructor מקבל כתובת URL של סקריפט קלאסי, שקשורה לכתובת ה-URL של המסמך. היא מחזירה מיד הפניה ל-worker instance החדש, שחושף ממשק להעברת הודעות וגם שיטת terminate() שעוצרת ומשמידה את ה-worker באופן מיידי.
const worker = new Worker('worker.js');
אפשר להשתמש בפונקציה importScripts() ב-Web Workers כדי לטעון קוד נוסף, אבל היא משהה את ההפעלה של ה-worker כדי לאחזר ולהעריך כל סקריפט. הוא גם מריץ סקריפטים בהיקף גלובלי כמו תג <script> קלאסי, כלומר המשתנים בסקריפט אחד יכולים להחליף את המשתנים בסקריפט אחר.
worker.js:
importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
// global to the whole worker
function sayHello() {
return 'world';
}
לכן, בעבר, ל-Web Workers הייתה השפעה עצומה על הארכיטקטורה של האפליקציה. המפתחים נאלצו ליצור כלים ופתרונות עקיפים חכמים כדי לאפשר שימוש ב-Web Workers בלי לוותר על שיטות פיתוח מודרניות. לדוגמה, כלי אריזה כמו webpack מטמיעים הטמעה קטנה של טוען מודולים בקוד שנוצר, שמשתמש ב-importScripts() לטעינת קוד, אבל עוטפים מודולים בפונקציות כדי למנוע התנגשויות של משתנים ולדמות ייבוא וייצוא של יחסי תלות.
הזנת עובדי מודול
ב-Chrome 80 יש מצב חדש ל-Web Workers עם היתרונות הארגונומיים והביצועיים של מודולים של JavaScript, שנקרא Module Workers. ה-constructor Worker מקבל עכשיו אפשרות חדשה, {type:"module"}, שמשנה את הטעינה וההפעלה של הסקריפט כך שיתאימו ל-<script type="module">.
const worker = new Worker('worker.js', {
type: 'module'
});
מכיוון ש-module workers הם מודולים רגילים של JavaScript, הם יכולים להשתמש בהצהרות ייבוא וייצוא. כמו בכל המודולים של JavaScript, יחסי התלות מופעלים רק פעם אחת בהקשר נתון (השרשור הראשי, ה-worker וכו'), וכל הייבואים העתידיים מפנים למופע המודול שכבר הופעל. הדפדפנים גם מבצעים אופטימיזציה לטעינה ולהרצה של מודולים של JavaScript. אפשר לטעון את יחסי התלות של מודול לפני שהמודול מופעל, וכך לטעון עצי מודולים שלמים במקביל. טעינת המודול גם שומרת במטמון את הקוד המנותח, כך שצריך לנתח רק פעם אחת את המודולים שמשמשים ב-thread הראשי וב-worker.
המעבר למודולים של JavaScript מאפשר גם להשתמש בייבוא דינמי כדי לטעון קוד באופן מדורג בלי לחסום את ההפעלה של ה-worker. ייבוא דינמי הוא הרבה יותר מפורש מאשר שימוש ב-importScripts() כדי לטעון יחסי תלות, כי הייצוא של המודול המיובא מוחזר במקום להסתמך על משתנים גלובליים.
worker.js:
import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}
כדי להבטיח ביצועים טובים, ה-method הישנה importScripts() לא זמינה ב-module workers. העברת ה-workers לשימוש במודולים של JavaScript פירושה שכל הקוד נטען במצב קפדני. שינוי בולט נוסף הוא שהערך של this בהיקף ברמה העליונה של מודול JavaScript הוא undefined, ואילו ב-workers קלאסיים הערך הוא ההיקף הגלובלי של ה-worker. למזלנו, תמיד היה self גלובלי שמספק הפניה להיקף הגלובלי. הוא זמין בכל סוגי ה-workers, כולל קובצי שירות (Service Workers), וגם ב-DOM.
טעינה מראש של קובצי Worker באמצעות modulepreload
אחד מהשיפורים המשמעותיים בביצועים שמתקבלים עם מודולים של ווב וורקרים הוא היכולת לטעון מראש את הווב וורקרים ואת יחסי התלות שלהם. כשמשתמשים ב-module workers, הסקריפטים נטענים ומופעלים כמודולים רגילים של JavaScript, כלומר אפשר לטעון אותם מראש ואפילו לנתח אותם מראש באמצעות modulepreload:
<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">
<script>
addEventListener('load', () => {
// our worker code is likely already parsed and ready to execute!
const worker = new Worker('worker.js', { type: 'module' });
});
</script>
גם ה-thread הראשי וגם ה-workers של המודולים יכולים להשתמש במודולים שנטענו מראש. האפשרות הזו שימושית למודולים שיובאו בשני ההקשרים, או במקרים שבהם אי אפשר לדעת מראש אם המודול ישמש ב-main thread או ב-worker.
בעבר, האפשרויות הזמינות לטעינה מראש של סקריפטים של Web Worker היו מוגבלות ולא בהכרח אמינות. למופעי worker קלאסיים היה סוג משאב "worker" משלהם לטעינה מראש, אבל אף דפדפן לא הטמיע את <link rel="preload" as="worker">. כתוצאה מכך, השיטה העיקרית לטעינה מראש של Web Workers הייתה להשתמש ב-<link rel="prefetch">, שהסתמכה לחלוטין על המטמון של HTTP. כשמשתמשים בזה בשילוב עם כותרות המטמון הנכונות, אפשר להימנע מהצורך להמתין להורדה של הסקריפט של ה-worker כדי ליצור מופע של ה-worker. עם זאת, בניגוד ל-modulepreload, הטכניקה הזו לא תמכה בטעינה מראש של יחסי תלות או בניתוח מראש.
מה לגבי קובצי worker משותפים?
החל מגרסה 83 של Chrome, ה-workers המשותפים עודכנו עם תמיכה במודולים של JavaScript. כמו ב-workers ייעודיים, כשיוצרים shared worker עם האפשרות {type:"module"}, הסקריפט של ה-worker נטען כמודול ולא כסקריפט קלאסי:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
לפני שהייתה תמיכה במודולים של JavaScript, ה-constructor של SharedWorker() ציפה רק לכתובת URL ולארגומנט אופציונלי של name. האפשרות הזו תמשיך לפעול בשימוש קלאסי ב-SharedWorker, אבל כדי ליצור מודול של SharedWorker צריך להשתמש בארגומנט החדש options. האפשרויות הזמינות זהות לאלה של וורקר ייעודי, כולל האפשרות name שמחליפה את הארגומנט הקודם name.
מה לגבי קובץ שירות (service worker)?
מפרט ה-Service Worker כבר עודכן כדי לתמוך בקבלת מודול JavaScript כנקודת הכניסה, באמצעות אותה אפשרות {type:"module"} כמו מודול Worker, אבל השינוי הזה עדיין לא הוטמע בדפדפנים. אחרי שזה יקרה, אפשר יהיה ליצור מופע של Service Worker באמצעות מודול JavaScript באמצעות הקוד הבא:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
עכשיו, כשהמפרט עודכן, דפדפנים מתחילים להטמיע את ההתנהגות החדשה. זה לוקח זמן כי יש כמה סיבוכים נוספים שקשורים להבאת מודולים של JavaScript ל-Service Worker. כדי לקבוע אם להפעיל עדכון, רישום של Service Worker צריך להשוות בין סקריפטים מיובאים לבין הגרסאות הקודמות שלהם שנשמרו במטמון. צריך להטמיע את זה במודולים של JavaScript כשמשתמשים בהם ל-Service Worker. בנוסף, במקרים מסוימים, כשבודקים אם יש עדכונים, קובצי ה-Service Worker צריכים להיות מסוגלים לעקוף את המטמון של הסקריפטים.
מקורות מידע נוספים וקריאה נוספת
- סטטוס התכונה, הסכמה בין דפדפנים ותקנים
- תוספת למפרט המקורי של מודול ה-workers
- מודולים של JavaScript ל-workers משותפים
- מודולים של JavaScript ל-Service Worker: סטטוס ההטמעה ב-Chrome