إطار Next.js

تحسين الأداء في Next.js

42 دقيقة الدرس 25 من 40

فهم الأداء في Next.js

تحسين الأداء أمر بالغ الأهمية لتقديم تطبيقات ويب سريعة ومستجيبة. توفر Next.js العديد من الميزات والأدوات المدمجة للمساعدة في تحسين أداء تطبيقك. يغطي هذا الدرس تقنيات متقدمة لتحليل وتحسين الأداء، مع التركيز على Core Web Vitals واستراتيجيات التحسين الحديثة.

نظرة عامة على Core Web Vitals

Core Web Vitals هي مقاييس Google لقياس تجربة المستخدم:

  • LCP (Largest Contentful Paint): يقيس أداء التحميل. يجب أن يحدث في غضون 2.5 ثانية.
  • FID (First Input Delay): يقيس التفاعلية. يجب أن يكون أقل من 100 ميلي ثانية.
  • CLS (Cumulative Layout Shift): يقيس الاستقرار البصري. يجب أن يكون أقل من 0.1.
  • INP (Interaction to Next Paint): يحل محل FID، يقيس الاستجابة. يجب أن يكون أقل من 200 ميلي ثانية.
مهم: تؤثر Core Web Vitals بشكل مباشر على تصنيفات SEO ورضا المستخدم. أعط الأولوية لتحسين هذه المقاييس.

تحسين الصور

يوفر مكون Image في Next.js تحسيناً تلقائياً:

// تحميل صور محسّن
import Image from 'next/image';

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/* تحسين تلقائي، تحميل كسول، وصور متجاوبة */}
      <Image
        src={product.image}
        alt={product.name}
        width={400}
        height={300}
        quality={85}
        placeholder="blur"
        blurDataURL={product.blurHash}
        priority={product.featured} // تحميل الصور المميزة على الفور
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
      <h3>{product.name}</h3>
      <p>{product.price} دولار</p>
    </div>
  );
}

// إنشاء عنصر نائب ضبابي
import { getPlaiceholder } from 'plaiceholder';

async function getBase64(imageUrl: string) {
  try {
    const res = await fetch(imageUrl);
    const buffer = await res.arrayBuffer();
    const { base64 } = await getPlaiceholder(Buffer.from(buffer));
    return base64;
  } catch {
    return 'data:image/svg+xml;base64,...'; // احتياطي
  }
}

تحسين الخطوط

قم بتحسين خطوط الويب باستخدام next/font:

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
import localFont from 'next/font/local';

// خطوط Google مع تحسين تلقائي
const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // منع النص غير المرئي أثناء التحميل
  variable: '--font-inter',
  preload: true,
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
  weight: ['400', '700'],
});

// خطوط محلية مخصصة
const myFont = localFont({
  src: './fonts/my-font.woff2',
  display: 'swap',
  variable: '--font-custom',
});

export default function RootLayout({ children }) {
  return (
    <html lang="ar" className={`${inter.variable} ${robotoMono.variable} ${myFont.variable}`}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}
نصيحة: استخدم font-display: swap لمنع النص غير المرئي أثناء تحميل الخط، مما يحسن الأداء المدرك.

تحليل الحزمة

قم بتحليل حجم حزمتك لتحديد فرص التحسين:

# تثبيت محلل الحزمة
npm install -D @next/bundle-analyzer

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // تكوين Next.js الخاص بك
  reactStrictMode: true,
  swcMinify: true,
});

# تشغيل تحليل الحزمة
ANALYZE=true npm run build

# هذا يفتح تصورات تفاعلية لحزمتك

تقسيم الكود والاستيراد الديناميكي

قسّم الكود لتقليل حجم الحزمة الأولي:

// استيراد ديناميكي للمكونات الثقيلة
import dynamic from 'next/dynamic';

// تحميل المكون فقط عند الحاجة
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>جاري تحميل المخطط...</p>,
  ssr: false, // تعطيل العرض من جانب الخادم إذا لم يكن مطلوباً
});

// استيراد ديناميكي مع صادرات مسماة
const DynamicModal = dynamic(
  () => import('@/components/Modal').then((mod) => mod.Modal),
  {
    loading: () => <div className="modal-skeleton" />,
  }
);

export default function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>لوحة التحكم</h1>
      <button onClick={() => setShowChart(true)}>إظهار المخطط</button>
      {showChart && <HeavyChart data={data} />}
    </div>
  );
}

التحميل الكسول مع React.lazy

استخدم التحميل الكسول المدمج في React للمكونات:

import { lazy, Suspense } from 'react';

// تحميل المكونات بشكل كسول
const CommentSection = lazy(() => import('@/components/CommentSection'));
const RelatedProducts = lazy(() => import('@/components/RelatedProducts'));

export default function ProductPage({ product }) {
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      {/* تحميل التعليقات فقط عند التمرير إلى العرض */}
      <Suspense fallback={<CommentsSkeleton />}>
        <CommentSection productId={product.id} />
      </Suspense>

      {/* تحميل المنتجات ذات الصلة بشكل كسول */}
      <Suspense fallback={<ProductsSkeleton />}>
        <RelatedProducts category={product.category} />
      </Suspense>
    </main>
  );
}

تحسين النصوص البرمجية

قم بتحسين تحميل النصوص البرمجية من جهات خارجية:

import Script from 'next/script';

export default function Layout({ children }) {
  return (
    <html>
      <body>
        {children}

        {/* التحميل بعد أن تصبح الصفحة تفاعلية */}
        <Script
          src="https://analytics.example.com/script.js"
          strategy="lazyOnload"
        />

        {/* التحميل قبل أن تصبح الصفحة تفاعلية (للنصوص الحرجة) */}
        <Script
          src="https://checkout.stripe.com/v3.js"
          strategy="beforeInteractive"
        />

        {/* التحميل بعد الترطيب (افتراضي) */}
        <Script
          src="https://widget.example.com/chat.js"
          strategy="afterInteractive"
          onLoad={() => {
            console.log('تم تحميل أداة الدردشة');
          }}
        />

        {/* نصوص مضمنة */}
        <Script id="analytics" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'GA_MEASUREMENT_ID');
          `}
        </Script>
      </body>
    </html>
  );
}
تحذير: يمكن أن تؤثر النصوص البرمجية من جهات خارجية بشكل كبير على الأداء. استخدم دائماً مكون Script مع الاستراتيجيات المناسبة بدلاً من علامات <script> العادية.

قياس الأداء باستخدام Lighthouse

استخدم Lighthouse لقياس وتحسين Core Web Vitals:

# تثبيت Lighthouse CLI
npm install -g lighthouse

# تشغيل تدقيق Lighthouse
lighthouse https://your-site.com --view

# إنشاء تقرير JSON
lighthouse https://your-site.com --output=json --output-path=./report.json

# التشغيل في CI/CD
lighthouse https://your-site.com --chrome-flags="--headless" --output=json

مراقبة Web Vitals

تتبع Core Web Vitals في الإنتاج:

// app/layout.tsx
import { Suspense } from 'react';
import { WebVitals } from '@/components/WebVitals';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Suspense>
          <WebVitals />
        </Suspense>
      </body>
    </html>
  );
}

// components/WebVitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // إرسال إلى التحليلات
    console.log(metric);

    // إرسال إلى Google Analytics
    if (window.gtag) {
      window.gtag('event', metric.name, {
        value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
        event_label: metric.id,
        non_interaction: true,
      });
    }

    // إرسال إلى نقطة نهاية التحليلات المخصصة
    fetch('/api/analytics/web-vitals', {
      method: 'POST',
      body: JSON.stringify(metric),
      headers: { 'Content-Type': 'application/json' },
    });
  });

  return null;
}

تحسين مكونات الخادم

استفد من مكونات الخادم لتحسين الأداء:

// مكون الخادم (افتراضي في دليل app)
async function ProductList() {
  // الجلب على الخادم - لا يؤثر على حزمة العميل
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // التخزين المؤقت لمدة ساعة واحدة
  }).then(r => r.json());

  return (
    <div className="product-grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// حافظ على مكونات العميل صغيرة ومركزة
'use client';

function ProductCard({ product }) {
  const [liked, setLiked] = useState(false);

  return (
    <div>
      <h3>{product.name}</h3>
      {/* فقط الأجزاء التفاعلية تحتاج إلى JS من جانب العميل */}
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

البث والتعليق

قم ببث المحتوى تدريجياً لتحسين الأداء المدرك:

import { Suspense } from 'react';

// إظهار هيكل فوري، بث المحتوى أثناء التحميل
export default function Page() {
  return (
    <main>
      {/* فوري: محتوى ثابت */}
      <h1>تفاصيل المنتج</h1>

      {/* البث 1: API سريع */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductInfo />
      </Suspense>

      {/* البث 2: API أبطأ */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews />
      </Suspense>

      {/* البث 3: API أبطأ */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </main>
  );
}

// كل مكون يبث بشكل مستقل
async function ProductReviews() {
  const reviews = await fetchReviews(); // يمكن أن يكون بطيئاً
  return <ReviewsList reviews={reviews} />;
}
نصيحة: استخدم حدود Suspense بشكل استراتيجي لإظهار المحتوى تدريجياً. يرى المستخدمون شيئاً مفيداً بشكل أسرع بدلاً من الانتظار حتى يتم تحميل كل شيء.

الجلب المسبق والتحميل المسبق

قم بتحسين التنقل باستخدام الجلب المسبق:

import Link from 'next/link';

export default function Navigation() {
  return (
    <nav>
      {/* جلب مسبق تلقائي عند التحويم/إطار العرض */}
      <Link href="/products" prefetch={true}>
        المنتجات
      </Link>

      {/* تعطيل الجلب المسبق للصفحات الأقل أهمية */}
      <Link href="/terms" prefetch={false}>
        الشروط
      </Link>
    </nav>
  );
}

// جلب مسبق يدوي مع الموجه
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function SmartPrefetch() {
  const router = useRouter();

  useEffect(() => {
    // جلب مسبق بعد أن يكون المستخدم خاملاً لمدة 2 ثانية
    const timer = setTimeout(() => {
      router.prefetch('/checkout');
    }, 2000);

    return () => clearTimeout(timer);
  }, [router]);

  return <button>الذهاب إلى الدفع</button>;
}

تحسين استعلامات قاعدة البيانات

قم بتحسين أنماط جلب البيانات:

// جلب البيانات المتوازي
async function getPageData(id: string) {
  // الجلب بالتوازي بدلاً من التسلسل
  const [product, reviews, recommendations] = await Promise.all([
    fetchProduct(id),
    fetchReviews(id),
    fetchRecommendations(id),
  ]);

  return { product, reviews, recommendations };
}

// جلب الحقول الانتقائي
async function getProducts() {
  // جلب الحقول المطلوبة فقط
  const products = await prisma.product.findMany({
    select: {
      id: true,
      name: true,
      price: true,
      image: true,
      // عدم جلب الحقول الثقيلة مثل الوصف
    },
    take: 20, // تحديد النتائج
    orderBy: { createdAt: 'desc' },
  });

  return products;
}

// استخدام نمط dataloader لمنع N+1
import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds) => {
  const users = await prisma.user.findMany({
    where: { id: { in: userIds } },
  });

  return userIds.map((id) => users.find((u) => u.id === id));
});

Edge Runtime لمسارات API

استخدم Edge Runtime للاستجابات الموزعة عالمياً ذات زمن الوصول المنخفض:

// app/api/fast/route.ts
export const runtime = 'edge';

export async function GET(request: Request) {
  // يعمل على شبكة الحافة بالقرب من المستخدمين
  const data = await fetch('https://api.example.com/data').then(r => r.json());

  return Response.json(data);
}

// مثال: استجابات على أساس الموقع الجغرافي
export async function GET(request: Request) {
  const country = request.headers.get('x-vercel-ip-country') || 'US';
  const city = request.headers.get('x-vercel-ip-city') || 'غير معروف';

  return Response.json({
    message: `مرحباً من ${city}، ${country}!`,
    timestamp: Date.now(),
  });
}

الضغط والتصغير

قم بتكوين التحسين التلقائي:

// next.config.js
module.exports = {
  // تمكين تصغير SWC (افتراضي في Next.js 13+)
  swcMinify: true,

  // ضغط الصور
  images: {
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60,
  },

  // تمكين الضغط
  compress: true,

  // إزالة console.log في الإنتاج
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },

  // تحسين استيرادات الحزمة
  modularizeImports: {
    'lodash': {
      transform: 'lodash/{{member}}',
    },
  },
};

رؤوس التخزين المؤقت

قم بتكوين رؤوس التخزين المؤقت للحصول على أداء أمثل:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=120',
          },
        ],
      },
    ];
  },
};

تمرين تطبيقي

المهمة: قم بتحسين صفحة منتج بطيئة للتجارة الإلكترونية:

  1. قم بتشغيل تدقيق Lighthouse وتحديد عوائق الأداء
  2. قم بتحسين جميع الصور باستخدام next/image وعناصر نائبة ضبابية
  3. قم بتحميل قسم المراجعات بشكل كسول باستخدام الاستيراد الديناميكي
  4. قم بتنفيذ البث باستخدام Suspense للتوصيات
  5. قم بإعداد مراقبة Web Vitals مع التحليلات
  6. قم بتحليل حجم الحزمة وتقليله بنسبة 20% على الأقل
  7. قم بتكوين رؤوس التخزين المؤقت المناسبة للأصول الثابتة
  8. حقق درجة أداء Lighthouse أعلى من 90

إضافي: قم بتنفيذ edge middleware لاختبار A/B دون التأثير على الأداء.

الملخص

النقاط الرئيسية حول تحسين الأداء في Next.js:

  • أعط الأولوية لـ Core Web Vitals: LCP وFID/INP وCLS
  • استخدم next/image للتحسين التلقائي للصور
  • قم بتحسين الخطوط باستخدام next/font وfont-display: swap
  • قم بتحليل وتقليل حجم الحزمة باستخدام الاستيراد الديناميكي
  • استخدم مكونات الخادم لتقليل JavaScript من جانب العميل
  • قم بتنفيذ البث التدريجي باستخدام Suspense
  • راقب الأداء في العالم الحقيقي باستخدام تتبع Web Vitals
  • قم بتكوين رؤوس التخزين المؤقت للحصول على تسليم أصول أمثل