שרשורי אינטרנט עם עובדי מודולים

עכשיו קל יותר להעביר מטלות כבדות לתוך שרשורי רקע באמצעות מודולי JavaScript ב-workers.

ג'ייסון מילר
ג'ייסון מילר

JavaScript פועל בשרשור יחיד, ופירוש הדבר שהוא יכול לבצע פעולה אחת בלבד בכל פעם. השיטה הזו אינטואיטיבית ופועלת היטב במקרים רבים באינטרנט, אבל היא עלולה להיות בעייתית כשאנחנו צריכים לבצע משימות כבדות משקל, כמו עיבוד, ניתוח, חישוב או ניתוח נתונים. ככל שיותר ויותר אפליקציות מורכבות מועברות באינטרנט, כך גובר הצורך בעיבוד מרובה שרשורים.

בפלטפורמת האינטרנט, ה-Web Workers API הוא המאפיין העיקרי של שרשורים במקבילות. סביבת Worker מפשטת בקלות את השרשורים של מערכות ההפעלה, שחושפים API המעביר הודעה לתקשורת בין שרשורים. זה יכול להיות שימושי מאוד בביצוע חישובים יקרים או בתפעול מערכי נתונים גדולים, כדי לאפשר ל-thread הראשי לפעול בצורה חלקה ולבצע את הפעולות היקרות בשרשור אחד או יותר של רקע.

הנה דוגמה אופיינית לשימוש של 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. מכיוון שלא הייתה מערכת מודול בזמן שהעובדים תוכננו, ה-API לטעינת קוד לעובד ולכתיבת סקריפטים נותר דומה לגישות של טעינת סקריפטים סינכרוניות שהיו נפוצות בשנת 2009.

היסטוריה: עובדים בגרסה הקלאסית

ה-worker מקבל כתובת URL של סקריפט קלאסי, שהיא יחסית לכתובת ה-URL של המסמך. היא מיד מחזירה הפניה למכונה החדשה של ה-worker, שחושפת ממשק להעברת הודעות וגם ל-method terminate() שעוצרת מיד והשמדה את ה-worker.

const worker = new Worker('worker.js');

ב-Workwork אפשר להשתמש בפונקציה importScripts() כדי לטעון קוד נוסף, אבל היא משהה את הביצוע של ה-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 בלי לוותר על שיטות הפיתוח המודרניות. לדוגמה, חבילות Bundle כמו Webpack מטמיעים קוד שנוצר באמצעות importScripts() לטעינת קוד, אבל ממירים מודולים בפונקציות כדי למנוע התנגשויות בין משתנים ולדמות ייבוא וייצוא של תלות.

יש להזין את עובדי המודול

מצב חדש לעובדי אינטרנט, עם הארגונומיה ויתרונות הביצועים של המודולים של JavaScript, נשלח ב-Chrome 80, שנקרא 'עובדי מודול'. ה-constructor Worker מקבל עכשיו אפשרות חדשה של {type:"module"}, שמשנה את הטעינה והביצוע של הסקריפט בהתאם ל-<script type="module">.

const worker = new Worker('worker.js', {
  type: 'module'
});

מאחר שעובדי מודולים הם מודולים סטנדרטיים של JavaScript, הם יכולים להשתמש בהצהרות ייבוא וייצוא. כמו בכל המודולים של JavaScript, יחסי תלות מבוצעים רק פעם אחת בהקשר נתון (thread ראשי, 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;
}

כדי להבטיח ביצועים מעולים, השיטה הישנה של importScripts() לא זמינה בעובדי מודולים. בעקבות החלפת עובדים במודולים של JavaScript, כל הקוד נטען במצב מחמיר. שינוי בולט נוסף הוא שהערך של this בהיקף העליון של מודול JavaScript הוא undefined, ואילו בגרסאות הקלאסיות של worker הערך הוא ההיקף הגלובלי של העובד. למרבה המזל, תמיד היה גלובלי self שמספק הפניה להיקף הגלובלי. היא זמינה לכל סוגי ה-worker, כולל קובצי שירות (service worker) וגם ב-DOM.

טעינה מראש של עובדים עם modulepreload

אחד מהשיפורים המשמעותיים בביצועים של עובדי המודול הוא היכולת לטעון מראש את העובדים ואת יחסי התלות שלהם. במודולים עובדים עם מודולים, הסקריפטים נטענים ומופעלים כמודולים רגילים של 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 הראשי וגם עובדי המודול יכולים להשתמש במודולים שנטענו מראש. האפשרות הזו שימושית במודולים שמיובאים בשני ההקשרים, או במקרים שבהם לא ניתן לדעת מראש אם ייעשה שימוש במודול ב-thread הראשי או בעובד.

בעבר, האפשרויות שהיו זמינות לטעינה מראש של סקריפטים של Web worker היו מוגבלות ולא מהימנות. לגרסה הקלאסית של worker היה סוג משאב "worker" משלהם לטעינה מראש, אבל לא הוטמעו בדפדפן <link rel="preload" as="worker">. כתוצאה מכך, השיטה הראשית שזמינה לטעינה מראש של עובדי אינטרנט הייתה שימוש ב-<link rel="prefetch">, שהסתמכה לחלוטין על מטמון ה-HTTP. בשילוב עם הכותרות הנכונות לשמירה במטמון, אפשרה למנוע מצב שבו יצירת העובד יצטרך להמתין להורדת הסקריפט של ה-worker. עם זאת, בניגוד ל-modulepreload, השיטה הזו לא תמכה ביחסי תלות מראש או בניתוח מראש.

מה לגבי עובדים משותפים?

החל מ-Chrome 83, העובדים המשותפים עודכנו בתמיכה במודולים של JavaScript. בדומה לעובדים ייעודיים, יצירת עובד משותף עם האפשרות {type:"module"} טוענת עכשיו את הסקריפט של העובד כמודול ולא כסקריפט קלאסי:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

לפני התמיכה במודולים של JavaScript, הבנאי SharedWorker() ציפה רק לכתובת URL ולארגומנט name אופציונלי. כך הוא ימשיך לפעול עבור השימוש הקלאסי של העובד המשותף. עם זאת, כדי ליצור מודול של עובדים משותפים עם מודול צריך להשתמש בארגומנט options החדש. האפשרויות הזמינות זהות לאפשרויות של worker ייעודי, כולל האפשרות name שמחליפה את הארגומנט name הקודם.

מה לגבי קובץ שירות (service worker)?

המפרט של קובץ השירות (service worker) כבר עודכן ויתמוך בקבלת מודול JavaScript כנקודת הכניסה, תוך שימוש באותה אפשרות {type:"module"} שיש לעובדי מודול, אבל השינוי הזה עדיין לא מיושם בדפדפנים. ברגע שזה יקרה, אפשר ליצור קובץ שירות (service worker) באמצעות מודול JavaScript באמצעות הקוד הבא:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

עכשיו, לאחר שהמפרט עודכן, דפדפנים מתחילים ליישם את ההתנהגות החדשה. התהליך הזה לוקח זמן כי יש סיבוכים נוספים שקשורים להבאת מודולים של JavaScript ל-Service Worker. במסגרת הרישום של קובץ השירות (service worker) צריך להשוות בין סקריפטים מיובאים לבין גרסאות המטמון הקודמות שלהם כדי להחליט אם להפעיל עדכון. את זאת צריך להטמיע במודולים של JavaScript כשמשתמשים ב-Service Workers. בנוסף, ב-Service Workers צריכה להיות אפשרות לעקוף את המטמון של סקריפטים במקרים מסוימים במהלך חיפוש של עדכונים.

מקורות מידע נוספים וחומרי קריאה נוספים