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

עכשיו קל יותר להעביר עומסי עבודה כבדים לשרשורי רקע בעזרת מודולים של JavaScript בעובדי האינטרנט.

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

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

הנה דוגמה אופיינית לשימוש של עובדים, שבה סקריפט של עובד מאזין להודעות מה-thread הראשי ומגיב על ידי שליחת הודעות משלו:

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

היסטוריה: Classic Works

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

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

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

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

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

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

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

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

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

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

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