עכשיו קל יותר להעביר משימות כבדות לשרשור ברקע באמצעות מודולים של 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. מכיוון שלא הייתה מערכת מודולים בזמן שהעובדים תוכננו, ה-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 בלי לוותר על שיטות הפיתוח המודרניות. לדוגמה, ב-Bunders כמו Webpack מוטמע הטמעה קטנה של טוען מודולים בקוד שנוצר שמשתמש ב-importScripts()
לטעינת קוד, אבל כולל מודולים בתוך פונקציות כדי למנוע התנגשויות בין משתנים, וכדי לדמות ייבוא וייצוא של תלות.
הזנת עובדים במודול
מצב חדש לעובדי אינטרנט, שכולל את יתרונות הארגונומית והביצועים של מודולים של JavaScript, יישלח ל-Chrome 80, שנקרא 'מעבדי מודולים'. האפשרות החדשה {type:"module"}
נוספה ל-constructor של Worker
, ומאפשרת לשנות את הטעינה וההפעלה של הסקריפט בהתאם ל-<script type="module">
.
const worker = new Worker('worker.js', {
type: 'module'
});
מכיוון שעובדי מודולים הם מודולים רגילים של JavaScript, הם יכולים להשתמש בדוחות ייבוא וייצוא. כמו בכל המודולים של 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
, השיטה הזו לא תמכה בעומס מראש של יחסי תלות או בניתוח מראש.
מה קורה עם עובדים משותפים?
נכון לגרסה 83 של Chrome, עודכנו עובדים משותפים במודולים של JavaScript. בדומה לעובדים ייעודיים, כשיוצרים קובץ Worker משותף עם האפשרות {type:"module"}
, הסקריפט של העובד נטען כמודול במקום כסקריפט קלאסי:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
לפני שהתמיכה במודולים של JavaScript נוספה, ה-constructor של SharedWorker()
ציפה רק לכתובת URL ולארגומנט name
אופציונלי. זה ימשיך לפעול לשימוש בגרסה הקלאסית של כוח עבודה משותף, אבל כדי ליצור פועלים משותפים במודול צריך להשתמש בארגומנט options
החדש. האפשרויות הזמינות זהות לאלו של כוח עבודה ייעודי, כולל האפשרות name
שמחליפה את הארגומנט הקודם name
.
מה לגבי קובץ שירות (service worker)?
המפרט של קובצי השירות כבר עודכן כדי לתמוך בקבלת מודול JavaScript כנקודת הכניסה, באמצעות אותה אפשרות {type:"module"}
כמו של מודולים של workers. עם זאת, השינוי הזה עדיין לא יושם בדפדפנים. לאחר מכן, תוכלו ליצור מופע של service worker באמצעות מודול JavaScript באמצעות הקוד הבא:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
עכשיו, אחרי שהמפרט עודכן, הדפדפנים מתחילים להטמיע את ההתנהגות החדשה. התהליך הזה נמשך זמן מה כי יש כמה סיבוכים נוספים שקשורים להעברת מודולים של JavaScript ל-service worker. רישום של קובץ שירות (service worker) צריך להשוות בין סקריפטים מיובאים לגרסאות קודמות שנשמרו במטמון כדי להחליט אם להפעיל עדכון. יש להטמיע אותו במודולים של JavaScript כאשר משתמשים ב-Service Workers. בנוסף, ל-service workers צריכה להיות אפשרות לדלג על המטמון של סקריפטים במקרים מסוימים כשהם בודקים אם יש עדכונים.
מקורות מידע נוספים ומידע נוסף
- סטטוס התכונה, הסכמה בין הדפדפנים וסטנדרטיזציה
- הוספת ספציפיקציה של עובדים במודול המקורי
- מודולים של JavaScript לעובדים משותפים
- מודולים של JavaScript ל-service worker: סטטוס ההטמעה של Chrome