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

עכשיו קל יותר להעביר משימות כבדות לשרשור ברקע באמצעות מודולים של JavaScript ב-Web Workers.

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

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

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

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

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

הפונקציה importScripts() זמינה בתוך משימות ה-web 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 נוסף מצב חדש לעובדים באינטרנט, שנקרא 'עובדים במודולים', עם יתרונות הארגונומיה והביצועים של מודולים של JavaScript. האפשרות החדשה {type:"module"} נוספה ל-constructor של Worker, ומאפשרת לשנות את הטעינה וההרצה של הסקריפט בהתאם ל-<script type="module">.

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

מאחר שעובדי המודולים הם מודולים רגילים של JavaScript, הם יכולים להשתמש בהצהרות import ו-export. כמו בכל המודולים של JavaScript, יחסי התלות מתבצעים רק פעם אחת בהקשר נתון (שרשור ראשי, worker וכו'), וכל הייבוא העתידיים מפנים למכונה של המודול שכבר בוצע. גם טעינת המודולים של JavaScript והרצה שלהם עוברים אופטימיזציה על ידי הדפדפנים. אפשר לטעון את יחסי התלות של מודול לפני שמפעילים אותו, וכך אפשר לטעון עצי מודולים שלמים במקביל. טעינת המודולים שומרת גם בזיכרון מטמון קוד שעבר ניתוח, כלומר מודולים שבהם נעשה שימוש בשרשור הראשי ובעובד צריך לנתח רק פעם אחת.

המעבר למודול JavaScript מאפשר גם להשתמש בייבוא דינמי לטעינה איטית של קוד בלי לחסום את הביצוע של העובד. ייבוא דינמי הוא הרבה יותר מפורש מאשר שימוש ב-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, ואילו בעובדים קלאסיים הערך הוא ההיקף הגלובלי של העובד. למרבה המזל, תמיד הייתה משתנה גלובלית בשם self שמספקת הפניה להיקף הגלובלי. הוא זמין בכל סוגי ה-workers, כולל קובצי שירות (service workers), וגם ב-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>

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

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

מה קורה עם עובדים משותפים?

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

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

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

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

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

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

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

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