ما وراء الحزمة: هندسة Dynamic Code-Splitting و Component Streaming في Expo
أستعرض في هذا المقال المعمارية التقنية لتفكيك حزم React Native الضخمة (Monolithic) باستخدام أحدث ميزات dynamic import و RSC في Expo. تعلم كيفية تحسين Time-to-Interactive من خلال عمل streaming للمكونات مباشرة إلى الجهاز.

ما وراء الحزمة: هندسة Dynamic Code-Splitting و Component Streaming في Expo
لسنوات، تعاملنا مع حزم React Native كأنها حاويات شحن ضخمة. كنا نضع كل ميزة، وكل مكتبة، وكل حالة استثنائية في ملف index.bundle واحد. مع نمو مشاريعي، كنت أراقب أوقات التحميل الأولية - وإحباط المطورين - وهي تزداد باطراد.
لكن اللعبة تغيرت الآن. مع التطورات الأخيرة في منظومة Expo وتطور Metro، أصبح لدينا أخيراً الأدوات اللازمة للتعامل مع معمارية تطبيقات الجوال بنفس الدقة (Granularity) التي نتمتع بها في الويب. أود أن أشارككم كيف قمت بتطبيق dynamic code-splitting و component streaming لتقليل TTI (Time-to-Interactive) بنسبة تقارب 40%.
مشكلة الـ Monolith
في تطبيق Expo القياسي، يتعين على محرك JavaScript (Hermes) تحميل الحزمة بالكامل في الذاكرة قبل أن تبدأ الشاشة الأولى حتى بالتفكير في عملية الـ rendering. إذا كان لديك تبويب 'Settings' معقد يحتوي على مكتبات رسم بياني (Charting libraries) ثقيلة، فإن المستخدمين يدفعون ثمن تلك الـ bytes بمجرد فتحهم لشاشة 'Home'.
أدركت أنه إذا أردنا التوسع للوصول إلى تجربة بمستوى المؤسسات الكبرى (Enterprise-grade)، علينا التوقف عن تقديم "العالم كله" دفعة واحدة.
الخطوة 1: الاستفادة من Metro’s Async Imports
الاختراق الأول بالنسبة لي كان إدراك أن Metro (أداة الحزم لـ React Native) يدعم الآن صيغة import() بشكل أصلي عند ضبطه بشكل صحيح. يسمح لنا هذا بإنشاء 'async chunks' - وهي ملفات منفصلة لا يتم جلبها إلا عند الحاجة إليها.
في معماريتي الأخيرة، ابتعدت عن الـ standard imports للعناصر الطرفية (Leaf-nodes) الثقيلة. إليكم كيف قمت بهيكلة الـ dynamic loader الخاص بنا:
من خلال تغليف هذه المكونات بـ React.lazy يقوم Metro تلقائياً بفصلها عن الحزمة الرئيسية. عندما ينتقل المستخدم إلى شاشة التحليلات (Analytics)، يقوم التطبيق بجلب ذلك الـ chunk المحدد عبر الشبكة (أو من الـ local cache).
الخطوة 2: الـ Splitting المستند إلى المسارات مع Expo Router
الـ manual lazy loading رائع، لكن السحر الحقيقي يحدث في طبقة الـ routing. باستخدام expo-router يمكننا تطبيق route-based splitting. وجدت أن أفضل نهج هو التعامل مع كل تبويب رئيسي كنقطة دخول (Entry point) منفصلة.
يتولى Expo Router تعقيدات الـ pre-fetching لهذه المسارات في الخلفية. قاعدتي الذهبية: إذا لم يلمس المستخدم ميزة ما في أول 30 ثانية له، فلا ينبغي أن تكون في الحزمة الأولية.
الخطوة 3: الآفاق الجديدة — React Server Components (RSC) في Expo
الاختراق الأكثر إثارة الذي كنت أختبره هو دعم Expo التجريبي لـ React Server Components. هذا ينقل النموذج من جلب البيانات (Data) إلى جلب المكونات التي تم عمل render لها (Rendered components).
في التدفق التقليدي، نقوم بجلب JSON، ثم يقوم العميل (Client) بعمل render لواجهة المستخدم. مع RSC في Expo، يمكن للسيرفر (أو edge function) عمل streaming لشجرة المكونات مباشرة. هذا يلغي الحاجة إلى شحن منطق الـ rendering لهذا المكون المحدد إلى حزمة العميل تماماً.
من خلال نقل المنطق الخاص بـ PostCard إلى السيرفر، تمكنت من إزالة آلاف الأسطر من منطق واجهة المستخدم من حزمة العميل. نحن نقوم أساساً بعمل streaming لـ UI.
المقايضات الهندسية (Engineering Trade-offs)
الأمر ليس دائماً وردياً. عندما تنتقل إلى معمارية مجزأة (Split architecture)، عليك حل مشاكل مثل:
- المرونة في حالة عدم الاتصال (Offline Resilience): ماذا يحدث إذا انتقل المستخدم إلى مسار lazy-loaded وهو في نفق؟ كان عليّ تطبيق pre-caching قوي باستخدام
expo-file-systemلضمان توفر الـ 'async' chunks محلياً قبل طلبها فعلياً. - حالة التنقل (Navigation State): إدارة الـ focus وأزرار العودة في الأجهزة أثناء حالة "التحميل" تتطلب استراتيجية
Suspenseقوية جداً.
النتيجة
في آخر عملية إعادة هيكلة كبرى قمت بها، انخفض حجم الحزمة الرئيسية لدينا من 4.2MB إلى 1.8MB. الفرق في الأداء الملحوظ هائل. لم يعد المستخدمون يحدقون في شاشة الـ splash بينما يقوم المحرك بمعالجة مكتبة لشاشة قد لا يروها أبداً.
الهندسة للجوال لم تعد تتعلق فقط بكتابة كود نظيف؛ بل تتعلق بإدارة كيفية تقديم هذا الكود. ما وراء الحزمة تكمن تجربة جوال أكثر مرونة وأسرع وحديثة حقاً. إذا لم تطلع على bundle visualizer الخاص بك مؤخراً، أقترح عليك بشدة البدء من هناك. ستفاجأ بحجم الأوزان الزائدة التي تحملها.