يحتوي محرك JavaScript في Safari على خلل خفي: عندما يتم استيراد نص الإدخال (entry script) الخاص بـ Web Worker بنمط الوحدات (module-style) في أي مكان آخر داخل الحزمة (bundle)، يقوم Safari بتنفيذ نص الإدخال هذا مرة ثانية. يؤدي هذا التشغيل المكرر إلى كسر حالة الـ singleton، مما يتسبب في تخريب العمال (workers) التي تعتمد على ذاكرة مخبئية مشتركة أو كائنات ذات نسخة واحدة (single-instance objects) بصمت.

ظهرت المشكلة أثناء تطوير تطبيق معالجة فيديو يعتمد على المتصفح يستخدم Web Workers لفك تشفير ملفات ProRes. تعامل Chrome و Firefox مع الكود دون أي مشاكل، لكن Safari فشل باستمرار في تحميل الفيديو. لم يبلغ الكونسول (console) إلا عن خطأ عام "cannot read video"، بينما كان المتسبب الحقيقي هو تشغيل كود تهيئة العامل مرتين، مما ترك نسختين مستقلتين من نفس الوحدة في الذاكرة.

كيف يظهر هذا الخلل

غالبًا ما تقوم أدوات التجميع الحديثة (Vite، Rollup، إلخ) بسحب الأدوات المشتركة إلى ملف إدخال العامل بحيث يمكن للأجزاء التي يتم تحميلها كسلاً (lazy-loaded chunks) استيراد ذلك الكود مرة أخرى من نقطة الإدخال. في المتصفحات التي تتبع سلوك محمل الوحدات (module loader) القياسي، بمجرد إنشاء وحدة الإدخال، يعيد المحمل نفس كائن الوحدة إلى أي عملية استيراد لاحقة، مما يمنع التنفيذ الثاني.

يختلف Safari عن هذا التوقع. فعندما يستورد جزء يتم تحميله كسلاً ملف إدخال العامل، يعامل Safari عملية الاستيراد هذه كطلب وحدة جديد ويعيد تنفيذ نص الإدخال. والنتيجة هي وجود نسختين منفصلتين من كل متغير أو فئة (class) أو singleton تم تعريفه هناك.

ما الذي يتعطل عند تشغيل الإدخال مرتين

  • الـ Singletons والذاكرة المخبئية (caches) لم تعد تشارك البيانات؛ حيث ترى إحدى النسخ ذاكرة مخبئية فارغة بينما تقوم الأخرى بملئها.
  • السجلات (Registries) (على سبيل المثال، قائمة بمعالجي الرسائل) تنتهي مقسمة بين النسختين، مما يترك أحد الجانبين فارغًا فعليًا.
  • مستمعو الأحداث (Event listeners) يتم إرفاقهم مرتين، مما قد يتسبب في معالجة مكررة أو تضخم في الذاكرة.
  • وحدات WebAssembly (WASM) يتم تحميلها مرتين، مما يهدر عرض النطاق الترددي ووقت التهيئة.
  • الفشل يكون صامتًا: لا يتم إطلاق أي استثناء (exception) غير ملتقط، بل تظهر المشكلة فقط في المنطق اللاحق الذي يعتمد على الحالة المفقودة.

اكتشاف المشكلة في المشروع

يمكن لعملية grep سريعة على الأصول المبنية (built assets) أن تكشف ما إذا كان أي جزء (chunk) يستورد ملف إدخال العامل:

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

إذا أدرج الأمر أي ملفات، فمن المرجح أن عمليات الاستيراد هذه هي التي تسبب خلل التشغيل المزدوج في Safari.

حلول عملية

  1. استخراج الكود المشترك من مدخل العامل.
    قم بتكوين أداة التجميع لوضع المكتبات المشتركة في جزء خاص بها (على سبيل المثال، باستخدام manualChunks في Rollup). بعد ذلك، يقوم كل من العامل وأي وحدات يتم تحميلها كسلاً باستيراد المكتبة من ذلك الملف الثالث، مما يلغي الحاجة إلى استيراد نقطة إدخال العامل.

  2. استخدام ملف إدخال بسيط (thin entry file).
    قم بتقليص نص إدخال العامل إلى سطر واحد يعيد تصدير التنفيذ الفعلي:

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

    طالما لا توجد حزمة أخرى تستورد worker-entry.js ، فلن يرى Safari طلب استيراد ثاني أبدًا، وبالتالي يعمل الإدخال مرة واحدة فقط.

يحافظ كلا النهجين على بقاء كود تهيئة العامل كنسخة واحدة (singleton) عبر التطبيق بأكمله.

لماذا يهم هذا الخلل

تعد Web Workers نمطًا شائعًا لتخفيف عبء العمليات الحسابية الثقيلة — مثل ترميز الفيديو، ومعالجة الصور، والتشفير — بعيدًا عن الخيط الرئيسي (main thread). يمكن لانقسام الحالة الصامت أن يحول ميزة تعمل بشكل مثالي إلى فشل متقطع يظهر فقط في Safari، وهو المتصفح الافتراضي لجزء كبير من أجهزة الكمبيوتر المكتبية والهواتف المحمولة. ولأن الخطأ يظهر كفشل عام في تحميل الوسائط، فقد يقضي المطورون ساعات في مطاردة العرض الخاطئ.

يسلط الخلل الضوء أيضًا على خطر أوسع: الاعتماد على دلالات محمل الوحدات (module-loader semantics) التي لا يتم تنفيذها بشكل موحد عبر المتصفحات. عندما تفترض استراتيجية التحسين لأداة التجميع وجود نسخة واحدة مشتركة من الوحدة، فإن أي انحراف يمكن أن يكسر هذا الافتراض.

وجهة نظر مغايرة وأسئلة مفتوحة

يتماشى سلوك Safari مع قواعد حل الوحدات (module resolution rules) الخاصة به، والتي تختلف قليلاً عن المواصفات القياسية في الحالات الحدية التي تتضمن العمال. يجادل بعض المطورين بأنه يجب على أدوات التجميع تجنب وضع الكود المشترك في ملف إدخال العامل تمامًا، مما يجعل المشكلة مسألة انضباط أثناء وقت البناء (build-time) بدلاً من كونها عيبًا في المتصفح. ويشير آخرون إلى أن انحراف Safari غير موثق، مما يترك المطورين دون وسيلة موثوقة لتوقعه.

لم تعترف Apple علنًا بالمشكلة، ولا يوجد جدول زمني معروف للإصلاح. وإلى أن يقوم Safari بتغيير المحمل الخاص به، تظل المسؤولية تقع على عاتق المطورين لإعادة هيكلة حزمهم أو إضافة منطق اكتشاف إلى خطوط أنابيب CI الخاصة بهم.

ما يجب متابعته لاحقاً

  • تحديثات المتصفح – تابع ملاحظات إصدار Safari بحثاً عن أي ذكر لكيفية التعامل مع module-worker.
  • إصلاحات مجتمع أدوات التجميع (Bundlers) – قد تقدم Vite وRollup وغيرها تحذيرات أو استراتيجيات تقسيم تلقائي (automatic chunking) لتجنب النمط الذي يتسبب في حدوث هذا الخطأ.
  • ممارسات الاختبار – إن دمج ملفات وسائط واقعية وإجراء اختبارات شاملة (full-stack) للـ worker على Safari قبل الإصدار يمكن أن يساعد في اكتشاف الفشل الصامت في وقت مبكر.

الخلاصة

إذا واجه مستخدمو Safari حالات فشل غير مفسرة تتعلق بالـ worker، فتحقق مما إذا كان أي bundle غير تابع للـ worker يقوم باستيراد نص الإدخال (entry script) الخاص بالـ worker. يؤدي خطأ التنفيذ المزدوج إلى تدمير حالة الـ singleton بصمت، ولكن نقل الكود المشترك خارج نقطة الإدخال (entry point) أو اختزال الإدخال ليصبح مجرد إعادة تصدير (re-export) بسيطة يعيد السلوك الصحيح دون الحاجة لانتظار إصلاح من المتصفح.