למנוע ה-JavaScript של Safari יש פגם נסתר: כאשר סקריפט הכניסה (entry script) של Web Worker בסגנון מודול מיובא למקום אחר ב-bundle, Safari מריצה את סקריפט הכניסה הזה פעם שנייה. ההרצה הכפולה שוברת את מצב ה-singleton, ומחבלת בשקט ב-workers המסתמכים על מטמנים (caches) משותפים או על אובייקטים מסוג single-instance.

הבעיה צצה במהלך פיתוח אפליקציית עיבוד וידאו מבוססת דפדפן המשתמשת ב-Web Workers כדי לפענח קבצי ProRes. Chrome ו-Firefox טיפלו בקוד ללא תקלות, אך Safari נכשל בעקביות בטעינת הווידאו. הקונסול דיווח רק על שגיאה כללית של "cannot read video", בעוד שהאשם האמיתי היה קוד האתחול של ה-worker שהורץ פעמיים והשאיר שתי עותקים עצמאיים של אותו מודול בזיכרון.

איך הבאג מתבטא

Bundlers מודרניים (Vite, Rollup וכו') נוהגים לעיתים קרובות למשוך עזרים (utilities) משותפים לקובץ הכניסה של ה-worker, כך ש-chunks שנטענים בצורה עצלה (lazy-loaded) יכולים לייבא את הקוד הזה בחזרה מנקודת הכניסה. בדפדפנים שעוקבים אחר התנהגות ה-module loader הסטנדרטית, ברגע שמודול הכניסה עובר instantiation, ה-loader מחזיר את אותו אובייקט מודול לכל import עוקב, מה שמונע הרצה שנייה.

Safari חורגת מהציפייה הזו. כאשר chunk שנטען בצורה עצלה מייבא את קובץ הכניסה של ה-worker, Safari מתייחסת ל-import כאל בקשת מודול חדשה ומריצה מחדש את סקריפט הכניסה. התוצאה היא שני מופעים (instances) נפרדים של כל משתנה, מחלקה (class) או singleton המוגדרים שם.

מה נשבר כשה-entry רץ פעמיים

  • Singletons ומטמנים (caches) כבר לא חולקים נתונים; עותק אחד רואה מטמן ריק בעוד שהשני ממלא אותו.
  • Registries (למשל, רשימה של מנהלי הודעות/message handlers) מסתיימים מפוצלים בין שני המופעים, מה שמותיר צד אחד ריק למעשה.
  • Event listeners מחוברים פעמיים, מה שעלול לגרום לטיפול כפול או לנפיחות בזיכרון (memory bloat).
  • מודולי WebAssembly (WASM) נטענים פעמיים, מה שגורם לבזבוז רוחב פס וזמן אתחול.
  • הכשל הוא שקט: לא נזרקת שגיאה (exception) לא נתפסת, אלא רק הלוגיקה בהמשך הדרך (downstream) התלויה במצב החסר מתנהגת בצורה שגויה.

זיהוי הבעיה בפרויקט

grep מהיר מול ה-assets שנבנו יכול לחשוף האם קיים chunk כלשהו המייבא את קובץ הכניסה של ה-worker:

grep -l 'from"./your.worker-' dist/assets/*.js

אם הפקודה מציגה קבצים כלשהם, ייתכן שה-imports הללו מפעילים את באג ההרצה הכפולה ב-Safari.

פתרונות מעקפים מעשיים

  1. חלץ קוד משותף מקובץ הכניסה של ה-worker.
    הגדר את ה-bundler כך שיניח ספריות משותפות ב-chunk משלהן (למשל, באמצעות manualChunks של Rollup). גם ה-worker וגם מודולים שנטענים בצורה עצלה יייבאו את הספרייה מהקובץ השלישי הזה, ובכך יבטלו את הצורך לייבא את נקודת הכניסה של ה-worker.

  2. השתמש בקובץ כניסה דק (thin entry file).
    צמצם את סקריפט הכניסה של ה-worker לשורה אחת שמייצאת מחדש (re-exports) את המימוש האמיתי:

    // worker-entry.js
    import("./main.js");
    

    כל עוד שום bundle אחר לא מייבא את worker-entry.js, Safari לעולם לא תראה בקשת import שנייה, ולכן ה-entry ירוץ רק פעם אחת.

שתי הגישות שומרות על קוד האתחול של ה-worker כ-singleton לאורך כל האפליקציה.

למה הבאג הזה חשוב

Web Workers הם תבנית נפוצה להעברת חישובים כבדים — קידוד וידאו, עיבוד תמונה, קריפטוגרפיה — מה-main thread. פיצול מצב (state split) שקט יכול להפוך פיצ'ר תקין לחלוטין לכשל לסירוגין שמופיע רק ב-Safari, הדפדפן ברירת המחדל בנתח גדול של מכשירי דסקטופ ומובייל. מכיוון שהשגיאה מופיעה כשגיאת טעינת מדיה כללית, מפתחים עלולים להקדיש שעות לרדיפת הסימפטום הלא נכון.

הבאג גם מדגיש סיכון רחב יותר: הסתמכות על סמנטיקה של module-loader שאינה מיושמת באופן אחיד בין דפדפנים. כאשר אסטרטגיית האופטימיזציה של bundler מניחה מופע מודול משותף יחיד, כל סטייה יכולה לשבור את ההנחה הזו.

נקודת מבט נגדית ושאלות פתוחות

ההתנהגות של Safari תואמת את חוקי פתרון המודולים (module resolution) שלה, השונים במעט מה-spec במקרי קצה המערבים workers. מפתחים מסוימים טוענים כי על bundlers להימנע לחלוטין מהצבת קוד משותף בקובץ הכניסה של worker, מה שהופך את הבעיה לעניין של משמעת בזמן הבנייה (build-time discipline) ולא לפגם בדפדפן. אחרים מציינים כי הסטייה של Safari אינה מתועדת, מה שמותיר מפתחים ללא דרך אמידה לצפות אותה מראש.

אפל לא הודתה בבעיה באופן פומבי, ואין לוח זמנים ידוע לתיקון. עד ש-Safari תשנה את ה-loader שלה, האחריות נותרת על המפתחים לבנות מחדש את ה-bundles שלהם או להוסיף לוגיקת זיהוי ל-CI pipelines שלהם.

מה כדאי לעקוב אחריו בהמשך

  • עדכוני דפדפן – עקבו אחר הערות השחרור של Safari לכל אזכור של טיפול ב-module-worker.
  • תיקוני קהילה עבור Bundlers – Vite, Rollup ואחרים עשויים להציג אזהרות או אסטרטגיות chunking אוטומטיות כדי להימנע מהתבנית שגורמת לתקלה.
  • שיטות בדיקה – שילוב קבצי מדיה מהעולם האמיתי ובדיקות worker מסוג full-stack ב-Safari לפני השחרור יכול לסייע בזיהוי מוקדם של הכשל השקט.

שורה תחתונה

אם משתמשי Safari שלכם חווים כשלים בלתי מוסברים הקשורים ל-worker, בדקו האם bundle שאינו worker מייבא את ה-entry script של ה-worker. באג ההרצה הכפולה הורס בשקט את ה-singleton state, אך העברת קוד משותף מחוץ ל-entry point או צמצום ה-entry ל-thin re-export מחזירה את ההתנהגות התקינה מבלי להמתין לתיקון בדפדפן.