Vite Bundle Split: قلّل JavaScript الأولي بدون إعادة كتابة التطبيق
هتكسب تحميل أول أسرع لو فصلت الكود الثقيل في Vite بدل ما ترسله لكل المستخدمين من أول زيارة. في المثال ده هننزل JavaScript الأولي من 1.8MB إلى 620KB، مع trade-off واضح: أول فتح للصفحة الثقيلة هيحتاج request إضافي.
مستوى القارئ: متوسط
المشكلة باختصار
الطريقة الشائعة الغلط إنك تسيب كل imports في main.tsx أو في route رئيسي واحد. الطريقة دي بتفشل لما التطبيق يكبر: صفحة التقارير، محرر Markdown، مكتبة charts، وPDF viewer يدخلوا في نفس bundle حتى لو المستخدم فتح صفحة Dashboard بسيطة.
الافتراض إن عندك تطبيق React أو Vue مبني بـ Vite، وفيه route ثقيل لا يفتحه كل المستخدمين. مثال واقعي: SaaS داخلي عنده 40 ألف زيارة يوميًا، لكن صفحة التقارير المتقدمة لا يفتحها إلا 12% من المستخدمين. إرسال مكتبة charts لكل الناس هنا تكلفة واضحة.
مثال بسيط قبل الشرح الدقيق
ركز في المثال ده: عندك مكتب استقبال. مش منطقي تحط كل ملفات المحاسبة، المخزن، والعقود على مكتب الموظف اللي هيسأل عن اسم العميل فقط. الأفضل إن ملف المحاسبة يطلع من الأرشيف لما الموظف يحتاجه.
في الواجهة نفس الفكرة بالظبط. الصفحة الأساسية تحتاج shell خفيف: navigation، auth state، وجدول بسيط. أما editor أو charting engine فده ملف منفصل يتم تحميله لما route يتفتح. علميًا، ده اسمه code splitting: تقسيم كود التطبيق إلى chunks أصغر، بحيث المتصفح يحمّل الجزء المطلوب أولًا ويؤجل الباقي.
الخطوة الأولى: افصل route ثقيل بـ dynamic import
ابدأ بأعلى route في الحجم. لا تبدأ بتقسيم 20 component صغير. أفضل طريقة إنك تفتح bundle analyzer أو تقيس ملفات dist/assets، ثم تختار route واضح مثل /reports.
// src/router.tsx
import { lazy, Suspense } from 'react';
import Dashboard from './pages/Dashboard';
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
export function AppRoutes() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/reports" element={<ReportsPage />} />
</Routes>
</Suspense>
);
}
الـ trade-off هنا إن أول دخول إلى /reports هيطلب chunk إضافي. المكسب إن المستخدم الذي لا يفتح التقارير لن يدفع تكلفة chart editor من البداية. لو عندك route يفتحه أقل من 30% من المستخدمين، غالبًا المكسب يستاهل.
الخطوة الثانية: امنع vendor chunk من التحول لصندوق واحد ضخم
بعد dynamic import، ممكن تلاقي vendor.js لسه كبير. السبب إن المكتبات كلها اتجمعت في chunk واحد. استخدم manualChunks بحذر لفصل المكتبات الثقيلة ذات الاستخدام المحدد.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
chunkSizeWarningLimit: 700,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/recharts')) return 'charts';
if (id.includes('node_modules/monaco-editor')) return 'editor';
if (id.includes('node_modules/react')) return 'react-vendor';
},
},
},
},
});
مهم: لا تعمل vendor واحد لكل node_modules بشكل أعمى. ده ممكن يثبت chunk ضخم في أول تحميل أو يزود cache invalidation. الأفضل تفصل المكتبات الثقيلة التي لها route أو feature واضح.
القياس قبل وبعد
في حالة قياس تقديرية مبنية على تطبيق تقارير متوسط، كانت النتيجة كالتالي: قبل التقسيم، أول route يحمّل حوالي 1.8MB JavaScript غير مضغوط، وزمن parse/compile على جهاز متوسط حوالي 640ms. بعد فصل التقارير والمحرر، initial JS نزل إلى 620KB، وزمن parse/compile نزل إلى 240ms. أول فتح لصفحة التقارير نفسها صار 980ms بدل 2.1s لأن chunk أصبح أوضح وأقل ازدحامًا.
قِس بالأوامر دي قبل وبعد. لا تعتمد على الإحساس.
npm run build
npx vite-bundle-visualizer --open
# قياس الملفات الناتجة
find dist/assets -name "*.js" -maxdepth 1 -exec ls -lh {} \;
# اختبار Lighthouse من CLI لو متاح عندك
npx lighthouse http://localhost:4173 --view
ما يجب الانتباه له
- زمن أول زيارة للصفحة الثقيلة: قد يزيد request إضافي. عالجه بـ prefetch بعد استقرار الصفحة الأساسية لو المستخدم غالبًا هيفتحها.
- تقسيم زائد: 30 chunk صغير ممكن يكون أسوأ من 4 chunks واضحة، خصوصًا على شبكات ضعيفة.
- side effects: بعض المكتبات تفترض ترتيب تحميل معين. Rollup نفسه ينبه إن manual chunks قد تغير سلوك التطبيق لو فيه side effects مبكرة.
- cache strategy: chunk ثابت للمكتبات الكبيرة جيد للكاش، لكن تقسيم خاطئ ممكن يخلي كل deploy يكسر كاش أكبر من اللازم.
متى لا تستخدم هذه الطريقة
لا تستخدمها لو التطبيق صغير أصلًا وinitial JS أقل من 250KB بعد الضغط. ولا تستخدمها لو كل المستخدمين يفتحون نفس الصفحة الثقيلة في أول 5 ثواني. في الحالة دي أنت لم تؤجل التكلفة، أنت فقط نقلتها إلى request ثاني. كذلك تجنب manualChunks لو الفريق لا يملك قياسًا واضحًا؛ التقسيم اليدوي بدون أرقام يتحول لصيانة مزعجة.
مصادر
- Vite Build Options: https://vite.dev/config/build-options.html
- Rollup output.manualChunks: https://rollupjs.org/configuration-options/#output-manualchunks
- MDN dynamic import: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
الخطوة التالية
افتح build الحالي، طلع أكبر 3 ملفات JavaScript، وافصل route واحد فقط بـ dynamic import. لو initial JS ما نزلش 20% على الأقل، ارجع للقياس بدل ما تكمل تقسيم عشوائي.