لماذا يتباطأ تطبيق Next.js الخاص بك: 7 حلول فعالة لتحسين الأداء
يستغرق تطبيق Next.js الخاص بك أكثر من 4 ثوانٍ للتحميل. وتبلغ درجة Lighthouse 60. ويقوم المستخدمون بمغادرة الموقع. إليك 7 حلول ملموسة مصحوبة بقياسات قبل وبعد — لا نصائح غامضة، بل كود برمجي فقط.
1. أنت ترسل 500 كيلوبايت من جافا سكريبت إلى المتصفح
قم بتشغيل الأمر npx @next/bundle-analyzer وتحقق من محتويات حزمة العميل الخاصة بك. العناصر التي تسبب زيادة الحجم عادةً: moment.js (300 كيلوبايت → استبدلها بـ date-fns)، lodash (70 كيلوبايت → استورد الوظائف المحددة)، مكتبات الرموز (استيراد جميع الرموز بدلاً من الرموز المحددة).
// سيئ: استيراد المكتبة بأكملها (70 كيلوبايت)
import _ from 'lodash';
const result = _.debounce(fn, 300);
// جيد: استيراد ما تحتاجه فقط (2 كيلوبايت)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// سيئ: استيراد جميع الرموز التي يزيد عددها عن 1000 (200 كيلوبايت)
import { ArrowRight } from 'lucide-react';
// جيد: استيراد قابل
للتنقية import ArrowRight from 'lucide-react/dist/esm/icons/arrow-right';2. كل مكون هو "استخدام العميل"
إذا قمت بإضافة "use client" إلى كل مكون، فأنت بذلك ترسل كل جافا سكريبت إلى المتصفح. أما مكونات الخادم (Server Components) فتُعرض على الخادم وترسل HTML — بدون أي جافا سكريبت. استخدم "use client" فقط للمكونات التي تحتاج إلى تفاعلية (onClick، useState، useEffect).
// مكون الخادم (افتراضي) - يرسل HTML، بدون جافا
سكريبت async function BlogList() {
const posts = await db.posts.findMany();
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
{/* الزر فقط هو جانب العميل */}
<LikeButton postId={post.id} />
</article>
))}
</div>
);
}
// مكون العميل - الجزء التفاعلي فقط'use
client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>Like</button>;
}3. الصور التي لا تحتوي على علامة `next/image`
تقوم علامات <img> الأولية بتحميل الصورة بالحجم الكامل. بينما تقوم علامة next/image تلقائيًا بتغيير حجم الصورة وتحويلها إلى صيغة WebP/AVIF، بالإضافة إلى التحميل المؤجل. وهذا وحده كفيل بتقليل مؤشر LCP بنسبة 40-60%.
// سيئ: يتم تحميل الصورة بالكامل بحجم
4 ميغابايت<img src="/hero.jpg" />
// جيد: يقوم Next.js بالتحسين والتحميل المؤجل وعرض الصورة بتنسيق
WebPimport Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // للصور الموجودة في الجزء
المرئي من الصفحة alt="صورة البطل"
/>4. استرداد البيانات على جانب العميل في الحالات التي يمكن فيها استخدام SSR
إذا كنت تستخدم useEffect + fetch لتحميل البيانات عند كل زيارة للصفحة، فإنك تجعل المستخدم ينتظر ما يلي: تنزيل HTML → تحليل JS → تهيئة React → طلب fetch → العرض. أما مع مكونات الخادم (Server Components)، فيتم جلب البيانات على الخادم ويحصل المستخدم على HTML الكامل على الفور.
5. عدم وجود رؤوس التخزين المؤقت في الموارد الثابتة
الملفات الثابتة في Next.js الموجودة ضمن المجلد /_next/static/ هي ملفات تم تجزئتها حسب المحتوى وهي غير قابلة للتعديل. يجب أن يقوم خادم nginx بتخزينها مؤقتًا بشكل مكثف:
# Nginx: تخزين ملفات Next.js الثابتة في ذاكرة التخزين المؤقت
لمدة عام واحد location /_next/static {
proxy_pass http://frontend:3000;
add_header Cache-Control "public, max-age=31536000, immutable";
}6. عدم استخدام React.lazy للمكونات الثقيلة
تعد المكونات مثل محررات النص المنسق والرسوم البيانية والخرائط كبيرة الحجم. قم بتحميلها بشكل مؤجل حتى لا تعيق عملية العرض الأولي:
import dynamic from 'next/dynamic';
// لا يتم التحميل إلا عند عرض المكون
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="h-64 animate-pulse bg-gray-800" />,
ssr: false, // تحتاج مكتبة المخططات إلى نافذة
});7. عدم تحسين الخطوط
تتسبب الخطوط المخصصة في حدوث تغير في التخطيط (CLS) عندما يتم تحميلها متأخراً. استخدم next/font لاستضافة الخطوط بنفسك وتحميلها مسبقاً:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // عرض الخط البديل حتى يتم تحميل الخط المطلوب
});
export default function Layout({ children }) {
return <body className={inter.className}>{children}</body>;
}النتائج المتوقعة
| النظام المتري | قبل | بعد إجراء جميع الإصلاحات |
|---|---|---|
| أداء لايت هاوس | 58 | 94 |
| أول رسم محتوى | 2.8 ثانية | 0.8 ثانية |
| أكبر عنصر يتم عرضه | 4.2 ثانية | 1.4 ثانية |
| حزمة JS الكاملة | 520 كيلوبايت | 180 كيلوبايت |
| التغير التراكمي في التخطيط | 0.25 | 0.02 |
