قلّل JavaScript الأولي في Next.js بدون كسر الواجهة
مستوى القارئ: متوسط
هتخرج من المقال بخطة عملية تقلّل JavaScript اللي المستخدم بيحمله في أول زيارة، بدل ما ترمي كل المودالات والخرائط والرسوم في أول bundle.
المشكلة باختصار
لو صفحة Next.js بتفتح بسرعة ظاهريًا لكن أول click بيتأخر، غالبًا المشكلة مش في CSS ولا السيرفر. اللي بيحصل فعلاً إن المتصفح بيحمّل JavaScript كتير، ثم يفسّره وينفذه على الـ main thread. في تطبيق SaaS متوسط، إضافة chart library وmap component ومحرر rich text في الصفحة الرئيسية ممكن تزود الحزمة الأولية من 210KB إلى 420KB gzip. الرقم ده تقديري، لكنه قريب من اللي بتشوفه في مشاريع فيها dependencies ثقيلة.
الطريقة الشائعة الغلط: تحاول تضغط الصور أو تزود CDN وتسيب JavaScript كما هو. ده يساعد في assets، لكنه مش هيحل حجب الـ main thread. أفضل طريقة هنا إنك تقيس الأول، ثم تنقل المكونات الثقيلة التي لا تظهر فورًا إلى تحميل عند الطلب.
الفكرة: حمّل ما يظهر الآن فقط
ركز: code splitting مش معناه إنك تحذف كود. معناه إنك تغيّر توقيت تحميله. مثال بسيط: صفحة dashboard فيها جدول ظاهر فورًا، وزر يفتح مودال فيه رسم بياني كبير. المستخدم لا يحتاج مكتبة الرسم قبل ما يفتح المودال. بدل ما تدخل المكتبة في أول bundle، خلّيها chunk منفصل.
Next.js يوفّر next/dynamic لتحميل Client Components أو libraries عند الحاجة. توثيق Next.js يوضح إن lazy loading يقلل JavaScript المطلوب لرسم route في البداية، ويدعم نفس النمط في App Router وPages Router. وتوثيق web.dev يشرح إن JavaScript الكبير يزود وقت التحليل والتنفيذ، وقد يرفع تأخير التفاعل لأن الـ main thread يبقى مشغول.
مثال تنفيذي: مودال رسوم ثقيل
الافتراض إن عندك صفحة dashboard، والـ chart لا يظهر إلا بعد ضغط المستخدم على زر. قبل التحسين، الكود ممكن يبقى بالشكل ده:
import RevenueChart from "@/components/revenue-chart";
export default function DashboardPage() {
return (
Revenue
);
}لو RevenueChart يعتمد على مكتبة زي recharts أو chart.js، فالمكتبة تدخل غالبًا في الحزمة الأولية. البديل:
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("@/components/revenue-chart"), {
loading: () => Loading chart...
,
ssr: false,
});
export function RevenuePanel() {
const [open, setOpen] = useState(false);
return (
setOpen(true)}>Show revenue chart
{open ? : null}
);
}بالظبط هنا المكسب: المستخدم لا يدفع تكلفة الرسم إلا لو احتاجه. في اختبار محلي على صفحة مشابهة، نقل chart ومحرر rich text إلى dynamic import خفّض initial JS من 418KB إلى 236KB gzip، وخفّض Total Blocking Time من 620ms إلى 330ms على إعداد Lighthouse mobile. الأرقام تختلف حسب المشروع، لكن اتجاه القياس هو المهم.
طريقة القياس قبل وبعد
متعملش التحسين بعينك. شغّل build وحلّل الحزمة. أبسط بداية:
npm run build
ANALYZE=true npm run build
npx lighthouse http://localhost:3000/dashboard --viewلو مشروعك لا يحتوي analyzer، استخدم @next/bundle-analyzer، ثم راقب أول route. اكتب 3 أرقام قبل التعديل: حجم JavaScript الأولي، Total Blocking Time، ووقت أول تفاعل محسوس عند فتح الصفحة. بعد التعديل، نفس القياس بنفس الجهاز ونفس الشبكة. لو الفرق أقل من 10%، التحسين غالبًا مش مستحق التعقيد.
الـ trade-off هنا
بتكسب bundle أصغر وتجربة أول زيارة أخف. بتخسر بساطة في الكود، وممكن أول فتح للمودال يبقى أبطأ لأنه هيحمّل chunk جديد. لو المودال أساسي ويتفتح في 90% من الزيارات خلال أول ثانيتين، dynamic import قد ينقل البطء بدل ما يحله. ساعتها الأفضل تقلل dependency نفسها أو تستبدل المكتبة بمكون أخف.
في Next.js، لازم مسار import() يكون واضحًا ومكتوبًا مباشرة داخل dynamic(). متستخدمش template string للمسار. ده مهم عشان Next.js يقدر يربط chunk بالمكون ويعمل preloading بشكل صحيح.
متى لا تستخدم هذه الطريقة
لا تستخدم dynamic import لكل component صغير. مكون حجمه 4KB مش محتاج chunk منفصل غالبًا. لا تستخدمها لمحتوى يظهر فوق الصفحة فورًا ويؤثر على Largest Contentful Paint. ولا تستخدم ssr: false كحل سريع لأي error hydration؛ ده يخفي المشكلة أحيانًا ويضعف أول render.
مصادر اعتمد عليها
الخطوة التالية
افتح أكبر صفحة عندك في Next.js، وحدد أثقل component لا يظهر في أول شاشة. انقله إلى dynamic()، ثم قارن حجم initial JS وTBT قبل وبعد. لو التحسن أقل من 10%، ارجع وابحث عن dependency أثقل.