Safari’s JavaScript engine has a hidden flaw: when a module-style Web Worker’s entry script is imported anywhere else in the bundle, Safari executes that entry script a second time. The duplicate run breaks singleton state, silently sabotaging workers that rely on shared caches or single-instance objects.

این مشکل هنگام توسعه یک اپلیکیشن پردازش ویدیو در مرورگر که از Web Workers برای رمزگشایی فایل‌های ProRes استفاده می‌کرد، خود را نشان داد. Chrome و Firefox کد را بدون هیچ مشکلی اجرا کردند، اما Safari همواره در بارگذاری ویدیو با شکست مواجه می‌شد. کنسول فقط یک خطای عمومی “cannot read video” گزارش می‌کرد، در حالی که مقصر اصلی، اجرای دو باره‌ی کد مقداردهی اولیه ورکر بود که باعث می‌شد دو کپی مستقل از همان ماژول در حافظه باقی بماند.

نحوه بروز این باگ

باندلرهای مدرن (Vite، Rollup و غیره) اغلب ابزارهای کاربردی مشترک را به فایل ورودی ورکر منتقل می‌کنند تا تکه‌های بارگذاری‌شونده با تأخیر (lazy-loaded chunks) بتوانند آن کد را دوباره از نقطه ورودی وارد کنند. در مرورگرهایی که از رفتار استاندارد بارگذار ماژول (module loader) پیروی می‌کنند، پس از نمونه‌سازی ماژول ورودی، بارگذار همان شیء ماژول را به هر دستور import بعدی برمی‌گرداند و از اجرای دوم جلوگیری می‌کند.

Safari از این انتظار فاصله می‌گیرد. وقتی یک تکه (chunk) که با تأخیر بارگذاری شده، فایل ورودی ورکر را import می‌کند، Safari با آن به عنوان یک درخواست ماژول جدید برخورد کرده و اسکریپت ورودی را دوباره اجرا می‌کند. نتیجه، ایجاد دو نمونه مجزا از هر متغیر، کلاس یا سینگلتونی است که در آنجا تعریف شده است.

وقتی ورودی دو بار اجرا می‌شود چه چیزهایی از کار می‌افتند؟

  • سینگلتون‌ها و کش‌ها دیگر داده‌ها را به اشتراک نمی‌گذارند؛ یک کپی کش خالی را می‌بیند در حالی که کپی دیگر آن را پر می‌کند.
  • رجیستری‌ها (برای مثال، لیستی از هندلرهای پیام) بین دو نمونه تقسیم می‌شوند و باعث می‌شوند یک سمت عملاً خالی بماند.
  • شنونده‌های رویداد (Event listeners) دو بار متصل می‌شوند که پتانسیل ایجاد مدیریت تکراری یا تورم حافظه (memory bloat) را دارد.
  • ماژول‌های WebAssembly (WASM) دو بار بارگذاری می‌شوند که باعث هدر رفتن پهنای باند و زمان مقداردهی اولیه می‌شود.
  • این شکست خاموش است: هیچ استثنای (exception) برخاسته‌ای رخ نمی‌دهد، بلکه فقط منطق‌های پایین‌دستی که به وضعیت مفقود شده وابسته هستند، دچار اختلال می‌شوند.

تشخیص مشکل در یک پروژه

یک دستور grep سریع روی دارایی‌های ساخته شده (built assets) می‌تواند نشان دهد که آیا تکه‌ای از کد، فایل ورودی ورکر را import می‌کند یا خیر:

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

اگر دستور لیستی از فایل‌ها را نمایش داد، احتمالاً آن importها باعث بروز باگ اجرای دوگانه در Safari می‌شوند.

راهکارهای عملی

  1. استخراج کد مشترک از ورودی ورکر.
    باندلر را طوری تنظیم کنید که کتابخانه‌های مشترک را در تکه (chunk) مخصوص به خود قرار دهد (مثلاً با استفاده از manualChunks در Rollup). در این صورت، هم ورکر و هم هر ماژول دیگری که با تأخیر بارگذاری می‌شود، کتابخانه را از آن فایل سوم import می‌کنند و نیاز به import کردن نقطه ورودی ورکر از بین می‌رود.

  2. استفاده از یک فایل ورودی سبک.
    اسکریپت ورودی ورکر را به یک خط کاهش دهید که پیاده‌سازی واقعی را مجدداً صادر (re-export) می‌کند:

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

    تا زمانی که هیچ باندل دیگری worker-entry.js را import نکند، Safari هرگز درخواست import دوم را نمی‌بیند، بنابراین ورودی تنها یک بار اجرا می‌شود.

هر دو روش باعث می‌شوند کد مقداردهی اولیه ورکر در کل اپلیکیشن به صورت سینگلتون (singleton) باقی بماند.

چرا این باگ اهمیت دارد؟

Web Workers الگوی رایجی برای انتقال محاسبات سنگین — مانند رمزگذاری ویدیو، پردازش تصویر و رمزنگاری — از ترد اصلی (main thread) هستند. یک تقسیم وضعیت خاموش می‌تواند یک ویژگی کاملاً کاربردی را به یک شکست متناوب تبدیل کند که فقط در Safari ظاهر می‌شود؛ مرورگری که در بخش بزرگی از دستگاه‌های دسکتاپ و موبایل به صورت پیش‌فرض استفاده می‌شود. از آنجایی که خطا به صورت یک شکست عمومی در بارگذاری رسانه ظاهر می‌شود، توسعه‌دهندگان ممکن است ساعت‌ها را صرف جستجو برای علتی اشتباه کنند.

این باگ همچنین یک ریسک گسترده‌تر را برجسته می‌کند: تکیه بر معناشناسی بارگذار ماژول (module-loader semantics) که به طور یکسان در تمام مرورگرها پیاده‌سازی نشده است. وقتی استراتژی بهینه‌سازی یک باندلر فرض را بر یک نمونه ماژول مشترک می‌گذارد، هرگونه انحراف می‌تواند آن فرض را از بین ببرد.

دیدگاه مخالف و سوالات بی‌پاسخ

رفتار Safari با قوانین حل ماژول (module resolution) خود همسو است که در موارد خاص مربوط به ورکرها، تفاوت‌های ظریفی با استاندارد دارد. برخی توسعه‌دهندگان استدلال می‌کنند که باندلرها باید کلاً از قرار دادن کد مشترک در فایل ورودی ورکر خودداری کنند و این موضوع را بیشتر یک مسئله انضباط در زمان ساخت (build-time discipline) می‌دانند تا یک نقص در مرورگر. دیگران اشاره می‌کنند که انحراف Safari مستند نشده است و توسعه‌دهندگان راه قابل اعتمادی برای پیش‌بینی آن ندارند.

اپل به طور عمومی این مشکل را تأیید نکرده است و هیچ جدول زمانی مشخصی برای رفع آن وجود ندارد. تا زمانی که Safari بارگذار خود را تغییر ندهد، مسئولیت بازسازی باندل‌ها یا افزودن منطق تشخیص به خط لوله‌های CI بر عهده توسعه‌دهندگان باقی می‌ماند.

آنچه در آینده باید زیر نظر داشت

  • به‌روزرسانی‌های مرورگر – یادداشت‌های انتشار Safari را برای هرگونه اشاره به مدیریت module-worker زیر نظر داشته باشید.
  • وصله‌های جامعه‌ی باندلر – Vite، Rollup و سایرین ممکن است هشدارها یا استراتژی‌های تکه‌تکه کردن (chunking) خودکار را برای اجتناب از الگویی که باعث بروز باگ می‌شود، معرفی کنند.
  • روش‌های تست – گنجاندن فایل‌های رسانه‌ای واقعی و انجام تست‌های کامل (full-stack) ورکر در Safari پیش از انتشار، می‌تواند این خطای پنهان را در مراحل اولیه شناسایی کند.

نکته کلیدی

اگر کاربران Safari شما با شکست‌های غیرقابل توضیح مرتبط با ورکر مواجه می‌شوند، بررسی کنید که آیا هیچ باندل غیر-ورکری، اسکریپت ورودی (entry script) ورکر را وارد (import) می‌کند یا خیر. باگ اجرای دوگانه به‌طور پنهان وضعیت سینگلتون (singleton state) را از هم می‌پاشد، اما انتقال کدهای مشترک از نقطه ورودی یا تبدیل نقطه ورودی به یک بازارسال (re-export) سبک، بدون نیاز به انتظار برای اصلاح مرورگر، رفتار صحیح را بازیابی می‌کند.