لماذا حزمة العميل في Next.js أكبر مما تظن: ملفات البراميل وoptimizePackageImports وحدود 'use client'
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗أهم النقاط
• ملف البراميل هو ملف index.ts وظيفته الوحيدة إعادة تصدير وحدات أخرى. استيراد رمز واحد منه يجبر أداة التحزيم على تحليل وتقييم كل وحدة يذكرها هذا الملف قبل أن تقرر ما يمكن حذفه.
• التوجيه 'use client' يحدد حدًا وليس ملفًا واحدًا. كل وحدة يستوردها مكوّن العميل، وكل وحدة تستوردها تلك الوحدات بدورها، تُترجم إلى حزمة المتصفح، ولهذا فإن إنزال التوجيه إلى الورقة التفاعلية الصغيرة هو أقوى أداة منفردة للتحكم في حجم الحزمة.
• الخيار experimental.optimizePackageImports في ملف next.config.ts يعيد كتابة استيرادات البراميل إلى مسارات وحدات مباشرة وقت البناء. تطبّقه Next.js تلقائيًا على قائمة مدمجة من الحزم مثل lucide-react و@mui/icons-material، أما أي حزمة أخرى فيجب إضافتها يدويًا.
• الحقل "sideEffects": false داخل ملف package.json هو الإشارة التي تسمح لأداة التحزيم بحذف التصديرات غير المستخدمة. أما الحزمة التي تشحن ملفات CSS فيجب أن تستخدم "sideEffects": ["*.css"] وإلا حُذفت ملفات التنسيق مع البقية.
• يطبع الأمر next build جدولًا لكل مسار، وعمود First Load JS فيه يمثل حجم جافاسكربت الذي يحمّله الزائر لهذا المسار متضمنًا الأجزاء المشتركة. هذا هو الرقم المستهدف، وأداة @next/bundle-analyzer مع ANALYZE=true تكشف أي وحدة حرّكته.
اكتشفنا الأمر بطريقة مملة. أثناء قراءة جدول المسارات في نهاية أمر next build لاحظنا أن صفحة تسويقية تحتوي على ثلاث فقرات ونموذج تسجيل تحمل تقريبًا نفس قيمة First Load JS الخاصة بلوحة التحكم. لم يكن في الصفحة أي شيء ثقيل، بل كان الجزء المشترك هو الثقيل، لأن مكوّن عميل صغير استورد Button من ملف براميل داخلي يعيد تصدير مكتبة المكوّنات بالكامل بما فيها الأيقونات والرسوم البيانية.
ما الذي ينتهي فعليًا داخل حزمة العميل في Next.js؟
كل ما يمكن الوصول إليه انطلاقًا من وحدة تحمل 'use client' يُشحن إلى المتصفح، بالإضافة إلى بيئة تشغيل React وNext.js وحمولة RSC الخاصة بالمسار. أما الوحدات التي تستوردها مكوّنات الخادم فقط فلا تُشحن. هذا هو النموذج كاملًا، ولهذا فإن موضع الحد أهم من أي إعداد في أداة التحزيم.
الخطأ الشائع هو مكوّن يتكوّن بنسبة 95% من ترميز ثابت مع عنصر تفاعلي واحد، ثم يوضع 'use client' في أعلى الملف. التوجيه ينتقل إلى الأسفل: مكتبة التواريخ ومجموعة الأيقونات ومخطط التحقق التي يستوردها الملف تتحول جميعها إلى كود عميل، رغم أن زرًا واحدًا فقط يحتاج معالج نقر. الحل هو إبقاء الصفحة مكوّن خادم واستخراج الجزء التفاعلي في ورقة عميل منفصلة.
تفصيلة واحدة توفر بايتات حقيقية: استيرادات الأنواع في TypeScript تُمحى، لكن فقط عندما يستطيع المترجم تمييز أنها أنواع. كتابة import type { User } from './models' تضمن المحو. وتفعيل verbatimModuleSyntax في ملف tsconfig.json، المتاح منذ TypeScript 5.0، يجعل القاعدة صريحة، لأن الاستيراد العادي يُخرج كاستيراد وقت تشغيل حتى لو كانت كل الأسماء التي يذكرها أنواعًا.
لماذا تضخّم ملفات البراميل الحزم وتبطئ خادم التطوير؟
ملف البراميل يجبر أداة التحزيم على تحليل كل وحدة يعيد تصديرها حتى لو استُخدم رمز واحد. في بناء الإنتاج تستطيع الأداة غالبًا إزالة الوحدات غير المستخدمة عبر tree shaking، أما في التطوير فلا تفعل ذلك، ولهذا يبدو التطبيق المليء بالبراميل بطيئًا في الترجمة عند كل طلب مسار بارد رغم أن الحزمة النهائية تبدو مقبولة.
كما أن tree shaking ليست مضمونة. فهي لا تحدث إلا إذا استطاعت الأداة إثبات خلو الوحدة من الآثار الجانبية، أي عدم وجود كود في المستوى الأعلى يعدّل شيئًا خارجها. وحدة واحدة معاد تصديرها تسجّل polyfill أو تعدّل prototype أو تستورد ملف تنسيق في مستواها الأعلى تثبّت نفسها في المخرجات، وتجرّ معها كل وحدة تستوردها.
بالنسبة لبراميل المشروع نفسه، الحل الموثوق هو استيراد الوحدة مباشرة بمسارها وحذف الملف البرميلي، أو الإبقاء عليه فقط لمجموعة صغيرة ثابتة من التصديرات التي يستخدمها الجميع. وقاعدة no-restricted-imports في ESLint التي تمنع مسار البرميل كمصدر استيراد هي ما منع برميلنا من العودة للنمو.
ماذا يفعل experimental.optimizePackageImports فعليًا؟
يعيد experimental.optimizePackageImports كتابة استيراد رمز محدد من حزمة إلى استيراد الملف الذي يعرّف هذا الرمز داخل الحزمة، وذلك وقت البناء، فلا تمر أداة التحزيم على برميل الحزمة إطلاقًا. تطبّق Next.js هذا تلقائيًا على قائمة مُحدَّثة من الحزم الشائعة، منها حزم الأيقونات والواجهات مثل lucide-react و@mui/icons-material. أما الحزم خارج القائمة، بما فيها حزم مساحة العمل الداخلية في المستودعات الموحدة، فيجب ذكرها صراحة في مصفوفة experimental.optimizePackageImports.
هناك قيدان يجب معرفتهما قبل الاعتماد عليه. الأول أنه يعمل على أسماء الحزم، لذا فإن اسمًا مستعارًا داخليًا مثل @/components ليس مرشحًا ويُعالج بحذف البرميل. والثاني أنه لا يستطيع إعادة كتابة استيراد النطاق: فالسطر import * as Icons from 'react-icons/fa' يبقي النطاق كاملًا حيًا لأن الوصول إلى أي خاصية فيه لا يُعرف إلا وقت التشغيل. ولا يزال الخيار الأقدم modularizeImports متاحًا للحزم التي يحتاج ترتيب ملفاتها إلى نمط يدوي.
كيف نقيس حجم حزمة العميل دون خداع أنفسنا؟
نقيس على بناء إنتاجي ونقارن قيمة First Load JS لكل مسار بين بنائين لنفس التطبيق. حزم التطوير غير مصغّرة ولم يجرِ عليها tree shaking وتحمل أدوات إعادة التحميل الساخن، لذا فإن أي رقم يُقرأ من next dev لا يقول شيئًا عما يحمّله المستخدمون.
نثبّت @next/bundle-analyzer كاعتمادية تطوير ونشغّل البناء بالمتغير ANALYZE=true. تغلّف الأداة إعدادات Next.js وتكتب تقارير خريطة شجرية منفصلة لحزم العميل وNode.js وEdge. نفتح تقرير العميل ونبدّل مقياس الحجم من parsed إلى gzipped ونقرأ أكبر المستطيلات، لأن الحجم المحلّل يضلّل بخصوص ما يعبر الشبكة فعليًا.
الانضباط الذي يجعل القياس مفيدًا هو تغيير واحد لكل بناء. فإطلاق الاستيراد المباشر وإعداد الضبط والاستيراد الديناميكي معًا يخبرك بالمجموع الذي تحرّك ولا يخبرك أي تغيير استحقه. نتجنب عمدًا ذكر أرقامنا قبل وبعد، لأن فرق حجم الحزمة حقيقة تخص شجرة اعتماديات واحدة ولا تنتقل إلى غيرها.
أي حل نختار؟
• الاستيراد المباشر للوحدة — تحلّل أداة التحزيم ملفًا واحدًا بدل برميل كامل. مناسب حين يكون البرميل داخليًا أو الحزمة خارج قائمة التحسين.
• optimizePackageImports — تُعاد كتابة استيرادات البراميل المسماة إلى مسارات عميقة وقت البناء. مناسب لحزمة خارجية ذات برميل كبير ومواضع استخدام كثيرة.
• إنزال 'use client' إلى الأسفل — الشجرة المستوردة تتوقف عن كونها كود عميل أصلًا. مناسب حين وُسم مكوّن شبه ثابت كعميل من أجل معالج واحد.
• next/dynamic — يؤجل البايتات إلى طلب ثانٍ بينما يبقى المجموع كما هو. مناسب حين لا يكون الكود لازمًا فعلًا للرسم الأول أو التفاعل الأول.
• استبدال الاعتمادية — يزيل البايتات نهائيًا. مناسب حين تكرر المكتبة واجهة موجودة في المنصة مثل Intl أو URLPattern.
متى نستخدم next/dynamic بدل تقليص الاستيراد؟
نستخدم next/dynamic حين لا يكون الكود لازمًا للرسم الأول فعلًا، مثل محرر نصوص غني أو خريطة أو مكتبة رسوم بيانية خلف تبويب. فهو يفصل الوحدة في جزء مستقل يُجلب عند الطلب. لكنه لا يصغّر التطبيق إجمالًا، بل ينقل البايتات إلى وقت لاحق، والجزء الذي يُجلب عند النقر هو جزء ينتظره المستخدم، فيظهر الأثر في تدهور مقياس Interaction to Next Paint بدل Largest Contentful Paint.
في موجّه التطبيق، الخيار ssr: false مسموح داخل مكوّن عميل فقط. واستدعاء dynamic() مع ssr: false من مكوّن خادم يسبب خطأ بناء، لأن مكوّن الخادم لا يملك مرحلة عرض على العميل ليتراجع عنها. ويجب دائمًا تمرير عنصر loading بنفس ارتفاع المكوّن الحقيقي، وإلا اشترى الجزء المؤجل إزاحة في التخطيط.
هل يهم حجم حزمة الخادم أيضًا؟
حجم حزمة الخادم يهم في البدء البارد لا في الشبكة. فدالة بلا خادم يجب تحميلها قبل تشغيلها، وشجرة اعتماديات ضخمة تجعل ذلك أبطأ، لذا يستحق إبقاء الحزم ذات الملفات الثنائية الأصلية أو بيئة التشغيل الكبيرة خارج الحزمة المتتبعة. والخيار serverExternalPackages في next.config.ts، وهو الاسم المستقر منذ Next.js 15 وكان سابقًا experimental.serverComponentsExternalPackages، يخبر Next.js بعدم تحزيم الحزمة وطلبها أصليًا وقت التشغيل. وحزم مثل sharp ومعظم برامج تعريف قواعد البيانات مكانها هنا.
العادة التي استقرينا عليها أبسط من كل هذه التقنيات: اقرأ جدول المسارات في كل بناء، وتعامل مع أي قفزة في First Load JS كما تتعامل مع اختبار فاشل. فالبرميل الذي كلّفنا جزءًا مشتركًا وصل في طلب دمج من سطر واحد لم يكن بوسع أي مراجع أن يشك فيه، بينما كان مخرج البناء سيكشفه في اليوم نفسه.
الأسئلة الشائعة
**س:** هل يزيل Turbopack ملفات البراميل تلقائيًا حتى تنتهي أهمية الموضوع؟
**ج:** بناء الإنتاج يزيل كثيرًا من التصديرات غير المستخدمة أيًا كانت أداة التحزيم، لكن tree shaking لا تزال عاجزة عن إزالة وحدة ذات آثار جانبية في مستواها الأعلى، كما أن بناء التطوير لا يجري tree shaking إطلاقًا. لذا يبقى الاستيراد المباشر هو الحل الذي يعمل في الوضعين.
**س:** هل First Load JS هو نفسه حجم جافاسكربت الخاص بالصفحة؟
**ج:** لا. First Load JS هو جزء المسار نفسه مضافًا إليه الأجزاء المشتركة التي يحمّلها كل مسار، ولهذا يرفع استيراد واحد سيئ في مكوّن مشترك الرقم في صفحات لا تستخدمه أصلًا.
**س:** هل نضبط "sideEffects": false في حزمة مساحة العمل الخاصة بنا؟
**ج:** نعم، إذا لم تكن أي وحدة فيها تفعل شيئًا وقت الاستيراد. وإذا كانت وحدة ما تستورد CSS أو تسجّل متغيرًا عامًا، استخدم صيغة المصفوفة مثل "sideEffects": ["*.css"] حتى تبقي أداة التحزيم تلك الملفات وتزيل الباقي.
**س:** هل يكسر حذف ملفات البراميل واجهة حزمة منشورة؟
**ج:** نعم إذا كان المستهلكون يستوردون من جذر الحزمة، لذا انشر خريطة exports بمداخل مسارات فرعية أولًا وأبقِ برميل الجذر لدورة إهمال واحدة. أما داخل تطبيق خاص فلا يوجد هذا العقد، ويمكن حذف البرميل فورًا.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.