Next.js: تحليل ومراقبة الأداء

آخر تحديث: 2026-08-26

الأداء هو سمة أساسية — في عصر الإنترنت حيث يمكن أن يؤدي تأخير 100 مللي ثانية إلى فقدان 7% من المستخدمين، يُعد تحسين الأداء مقياسًا جوهريًا للقدرة التنافسية للمنتج.

1. ما ستتعلمه



2. قصة حقيقية لمهندسة أداء

(1) نقطة الألم: درجة Lighthouse 45؛ المستخدمون يغادرون

ديانا مهندسة أداء في منصة TaskFlow. تلقت مؤخرًا تقرير تحليل سلوك المستخدم:

المقياس القيمة الحالية المعيار الصناعي التأثير
LCP 4.8s < 2.5s معدل ارتداد المستخدم 45%
FID/INP 320 ms < 200 ms التفاعل يبدو متقطعًا
CLS 0.35 < 0.1 نقرات خاطئة بسبب تغيرات التخطيط
حجم الحزمة 1.2 MB < 500 KB تحميل بطيء للشاشة الأولى

والأسوأ من ذلك، أنها لم تستطع الإجابة عن الأسئلة التالية:

(2) حلول لأنظمة مراقبة الأداء

أنشأت ديانا نظام مراقبة أداء شامل:

TSX
// Real-User Monitoring(RUM)
import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals(metric => {
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify(metric)
    })
  })
}

(3) النتائج

البعد قبل التحسين بعد التحسين
LCP 4.8s 1.2s
حجم الحزمة 1.2 MB 380 KB
درجة Lighthouse 45/100 92/100
اكتشاف تدهور الأداء يُكتشف بعد النشر يُعترض أثناء مرحلة PR


3. Lighthouse CI

يقوم Lighthouse CI تلقائيًا بتشغيل تدقيقات Lighthouse في خط أنابيب CI/CD ويحدد حدود ميزانية الأداء.

100%
graph TB
    A[CI Pipeline] --> B[Lighthouse CI]
    B --> C[Run Lighthouse Audit]
    C --> D{Comparing Performance and Budget}
    D -->|Through| E[Pipeline Continue]
    D -->|Failure| F[Pipeline Interruption]
    F --> G[PR Report a Comment Failure]
    G --> H[Developers Optimizing Locally]
    
    style A fill:#cce5ff
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#d4edda

(1) إعداد lighthouserc.js

JS
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // عدد مرات الجمع (لإيجاد الوسيط)
      numberOfRuns: 3,
      
      // الصفحات المراد تدقيقها
      url: [
        'http://localhost:3000',
        'http://localhost:3000/login',
        'http://localhost:3000/dashboard',
        'http://localhost:3000/projects',
        'http://localhost:3000/projects/p1'
      ],
      
      // بدء خادم تطوير Next.js
      startServerCommand: 'npm run start -p 3000',
      startServerReadyPattern: 'ready started server',
      
      // محاكاة الجهاز
      settings: {
        preset: 'desktop',
        throttling: {
          cpuSlowdownMultiplier: 4,
          downloadThroughputKbps: 10000,
          uploadThroughputKbps: 5000,
          rttMs: 40
        }
      }
    },
    
    assert: {
      // التحكم بالوصول على أساس الأداء
      budgets: [
        {
          path: '/',
          resourceSizes: [
            { resourceType: 'total', budget: 500 * 1024 },     // 500KB
            { resourceType: 'script', budget: 200 * 1024 },    // 200KB
            { resourceType: 'image', budget: 150 * 1024 }      // 150KB
          ],
          resourceCounts: [
            { resourceType: 'script', budget: 15 },
            { resourceType: 'stylesheet', budget: 5 },
            { resourceType: 'image', budget: 20 }
          ]
        }
      ],
      
      // حدود درجة Lighthouse
      assertions: {
        // درجة الفئة
        'categories:performance': ['warn', { minScore: 0.9 }],
        'categories:accessibility': ['warn', { minScore: 0.9 }],
        'categories:best-practices': ['warn', { minScore: 0.9 }],
        'categories:seo': ['warn', { minScore: 0.9 }],
        
        // Core Web Vitals
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 200 }],
        
        // مقاييس رئيسية أخرى
        'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
        'interactive': ['warn', { maxNumericValue: 3500 }],
        'max-potential-fid': ['warn', { maxNumericValue: 100 }],
        
        // أفضل الممارسات
        'uses-http2': ['error'],
        'uses-responsive-images': ['error'],
        'offscreen-images': ['error'],
        'unused-javascript': ['warn', { maxNumericValue: 50 * 1024 }],
        'unused-css-rules': ['warn', { maxNumericValue: 10 * 1024 }],
        'uses-optimized-images': ['error'],
        'uses-text-compression': ['error'],
        'uses-rel-preconnect': ['warn'],
        'uses-rel-preload': ['warn'],
        'efficient-animated-content': ['warn'],
        'total-byte-weight': ['error', { maxNumericValue: 500 * 1024 }]
      }
    },
    
    upload: {
      target: 'temporary-public-storage'
    },
    
    server: {
      // السماح بالروابط الخارجية (بيئة CI)
      allowStaticServer: true
    }
  }
}

(2) دمج CI

YAML
# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build application
        run: npm run build
      
      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}

▶ مثال: تفسير تقارير Lighthouse CI

TEXT 📖 للعرض فقط
Lighthouse CI Results (3 runs, median)

URL: http://localhost:3000/dashboard

┌──────────────┬─────────┬─────────┬──────┐
│ Category     │ Score   │ Target  │ Pass │
├──────────────┼─────────┼─────────┼──────┤
│ Performance  │ 92      │ ≥ 90    │ ✅   │
│ Accessibility│ 95      │ ≥ 90    │ ✅   │
│ Best Practice│ 100     │ ≥ 90    │ ✅   │
│ SEO          │ 100     │ ≥ 90    │ ✅   │
└──────────────┴─────────┴─────────┴──────┘

Core Web Vitals
  LCP: 1,423 ms  (≤ 2,500 ms)  ✅
  TBT: 87 ms     (≤ 200 ms)   ✅
  CLS: 0.05      (≤ 0.1)      ✅

Budgets
  Total KB: 382 of 500 KB  ✅
  Scripts:  15 of 15       ✅
  Images:   4 of 20        ✅


4. Bundle Analyzer

@next/bundle-analyzer يعرض حجم وحدات JavaScript المضغوطة بصريًا لمساعدتك في تحديد التبعيات كبيرة الحجم.

100%
graph LR
    A[Build Process] --> B[Bundle Analyzer Plugin]
    B --> C[Generate treemap HTML]
    C --> D[Browser Open Analysis]
    D --> E{Identifying Large Modules}
    E --> F[tree-shaking Not in effect]
    E --> G[Duplicate Dependencies]
    E --> H[A library that is too large has been loaded]
    
    style A fill:#cce5ff
    style C fill:#d4edda

(1) إعداد Bundle Analyzer

JS
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
  openAnalyzer: true,
  analyzerMode: 'static',
  reportFilename: 'bundle-report.html',
  defaultSizes: 'gzip'
})

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  productionBrowserSourceMaps: false,
  swcMinify: true
}

module.exports = withBundleAnalyzer(nextConfig)

(2) التحليل التشغيلي

JSON
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build",
    "analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
  }
}

▶ مثال: تفسير تقرير تحليل الحزمة

BASH
ANALYZE=true npm run build
TEXT 📖 للعرض فقط
Bundle Report — client/gzip

Module                          Size      % of Total
├── node_modules               280 KB    73.3%
│   ├── @radix-ui              85 KB     22.3%
│   ├── react-dom              52 KB     13.6%
│   ├── react                  18 KB      4.7%
│   ├── date-fns               45 KB     11.8%
│   ├── recharts               32 KB      8.4%
│   └── lodash                 28 KB      7.3%
├── components/                72 KB     18.8%
│   ├── Dashboard.tsx          18 KB      4.7%
│   ├── ProjectList.tsx        12 KB      3.1%
│   ├── TaskCard.tsx           8 KB       2.1%
│   └── ...
├── pages/                     22 KB      5.8%
└── lib/                       8 KB       2.1%

Total: 382 KB (gzip)

(3) استراتيجيات تحسين الحزمة

الاستراتيجية الطريقة التوفير المتوقع
الاستيراد الديناميكي const Chart = dynamic(() => import('./Chart')) -280 KB تحميل أولي
Tree Shaking استخدام استيرادات ESM بدلاً من CJS -20% كود ميت
تقسيم الكود تقسيم تلقائي حسب المسار (افتراضي في Next.js) -30% JavaScript في الشاشة الأولى
استبدال التبعيات date-fnsdayjs (45KB ← 6KB) -39 KB
التحميل الكسول React.lazy + Suspense -85 KB مكتبة تفاعل


5. مراقبة Core Web Vitals للمستخدمين الحقيقيين

توفر Lighthouse بيانات معملية؛ فقط مراقبة المستخدم الحقيقي (RUM) يمكنها أن تعكس تجربة المستخدم الفعلية.

100%
graph TB
    A[User's browser] --> B[Web Vitals Data Collection]
    B --> C{Indicator Type}
    C --> D[LCP Maximum Content Rendering]
    C --> E[INP Defer to the next draw call]
    C --> F[CLS Cumulative Layout Shift]
    C --> G[FCP Initial Content Rendering]
    C --> H[TTFB ByteTime]
    D --> I[Report to Analytics Services]
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J[PostHog / GA4 / Self-built]
    J --> K[Dashboard Visualization]
    
    style B fill:#cce5ff
    style I fill:#fff3cd
    style K fill:#d4edda

(1) مكون إبلاغ Web Vitals

TSX
// src/components/WebVitals.tsx
'use client'

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

type MetricType = {
  id: string
  name: string
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  delta: number
}

export function WebVitals() {
  useReportWebVitals((metric: MetricType) => {
    // الإبلاغ إلى خدمة التحليلات
    const body = {
      metric_name: metric.name,
      metric_value: metric.value,
      metric_rating: metric.rating,
      metric_delta: metric.delta,
      url: window.location.pathname,
      user_agent: navigator.userAgent,
      device_type: getDeviceType(),
      connection: (navigator as any).connection?.effectiveType || 'unknown',
      timestamp: new Date().toISOString()
    }

    // استخدام sendBeacon لضمان الإبلاغ عن البيانات حتى عند إلغاء تحميل الصفحة
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/vitals', JSON.stringify(body))
    } else {
      fetch('/api/vitals', {
        method: 'POST',
        body: JSON.stringify(body),
        keepalive: true
      })
    }
  })

  return null
}

function getDeviceType(): string {
  const width = window.innerWidth
  if (width < 768) return 'mobile'
  if (width < 1024) return 'tablet'
  return 'desktop'
}

(2) مسار API لتخزين بيانات vitals

TS
// src/app/api/vitals/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function POST(request: NextRequest) {
  try {
    const data = await request.json()
    
    // الحفظ في قاعدة البيانات
    await prisma.webVital.create({
      data: {
        metricName: data.metric_name,
        metricValue: data.metric_value,
        metricRating: data.metric_rating,
        url: data.url,
        deviceType: data.device_type,
        connection: data.connection,
        userAgent: data.user_agent,
        timestamp: new Date(data.timestamp)
      }
    })
    
    // إذا كان المؤشر ضعيفًا، تشغيل تنبيه
    if (data.metric_rating === 'poor') {
      await checkAlertThresholds(data)
    }
    
    return NextResponse.json({ ok: true }, { status: 200 })
  } catch (error) {
    console.error('Failed to store web vital:', error)
    return NextResponse.json({ ok: false }, { status: 500 })
  }
}

async function checkAlertThresholds(metric: any) {
  // النظر إلى آخر 5 دقائق، عدد المؤشرات الضعيفة للمسار
  const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000)
  
  const poorCount = await prisma.webVital.count({
    where: {
      metricName: metric.metric_name,
      metricRating: 'poor',
      url: metric.url,
      timestamp: { gte: fiveMinAgo }
    }
  })
  
  // إذا تجاوز الحد، إرسال تنبيه
  if (poorCount > 10) {
    // await sendAlert(`Performance degradation detected on ${metric.url}: ${metric.metric_name} = ${metric.metric_value}`)
    console.warn(`ALERT: ${poorCount} poor ${metric.metric_name} on ${metric.url}`)
  }
}

(3) استعلام لوحة تحكم الأداء

TS
// src/app/dashboard/performance/page.tsx
import { prisma } from '@/lib/prisma'

async function getPerformanceSummary() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  
  const vitals = await prisma.webVital.groupBy({
    by: ['metricName'],
    where: {
      timestamp: { gte: today }
    },
    _avg: {
      metricValue: true
    },
    _count: true
  })
  
  // حساب نسبة good / needs-improvement / poor
  const ratings = await prisma.webVital.groupBy({
    by: ['metricRating'],
    where: {
      timestamp: { gte: today }
    },
    _count: true
  })
  
  return { vitals, ratings }
}

▶ مثال: تفسير بيانات Web Vitals

TEXT 📖 للعرض فقط
Today's Core Web Vitals(2026-07-06)

Metric       │ P75     │ P95     │ Good %  │ Poor %
─────────────┼─────────┼─────────┼─────────┼───────
LCP          │ 1,234ms │ 3,567ms │ 85.2%   │ 5.1%
INP          │ 98ms    │ 245ms   │ 91.3%   │ 2.8%
CLS          │ 0.05    │ 0.18    │ 88.7%   │ 4.2%
FCP          │ 823ms   │ 1,945ms │ 90.1%   │ 3.5%
TTFB         │ 345ms   │ 890ms   │ 92.4%   │ 2.1%

Top 5 Slow Routes
/dashboard          LCP: 3.2s  (200 visits)
/projects/p1/tasks  LCP: 2.8s  (150 visits)
/reports            LCP: 2.6s  (80 visits)
/settings           LCP: 2.1s  (60 visits)
/analytics          LCP: 1.9s  (120 visits)


6. دمج PostHog Speed Insights

PostHog هو منصة تحليلات منتج مفتوحة المصدر تتضمن تسجيل الجلسات المدمج وfeature flags وSpeed Insights.

(1) الدمج من جانب العميل

TSX
// src/components/PostHogProvider.tsx
'use client'

import { posthog } from 'posthog-js'
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react'
import { useEffect } from 'react'
import { useReportWebVitals } from 'next/web-vitals'

if (typeof window !== 'undefined') {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
    capture_pageview: false,
    capture_performance: true,  // التقاط تلقائي لـ Web Vitals
    loaded: (ph) => {
      if (process.env.NODE_ENV === 'development') ph.opt_out_capturing()
    }
  })
}

export function PostHogWebVitals() {
  const posthog = usePostHog()

  useReportWebVitals((metric) => {
    posthog.capture('$web_vitals', {
      $metric_name: metric.name,
      $metric_value: metric.value,
      $metric_rating: metric.rating,
      $pathname: window.location.pathname,
      $device: navigator.userAgent
    })
  })

  return null
}

export function PHProvider({ children }: { children: React.ReactNode }) {
  return <PHProvider client={posthog}>{children}</PHProvider>
}

(2) الدمج في التخطيط الجذري

TSX
// src/app/layout.tsx
import { PHProvider, PostHogWebVitals } from '@/components/PostHogProvider'
import { WebVitals } from '@/components/WebVitals'

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <PHProvider>
          {children}
          <WebVitals />
          <PostHogWebVitals />
        </PHProvider>
      </body>
    </html>
  )
}


7. /instrumentation.ts تتبع مخصص

يدعم Next.js 16 تسجيل تتبعات OpenTelemetry عبر instrumentation.ts لمراقبة أداء جانب الخادم والمسارات البطيئة.

TS
// src/instrumentation.ts
// ============================================
// Next.js 16 تتبع مخصص
// ============================================
import { registerOTel } from '@vercel/otel'

export async function register() {
  registerOTel({
    serviceName: 'taskflow',
    attributes: {
      'deployment.environment': process.env.NODE_ENV,
      'service.version': process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'unknown'
    }
  })
}

(1) تتبع Span مخصص

TS
// src/lib/tracing.ts
// تتبع Span مخصص
const SPAN_NAMES = {
  DATABASE_QUERY: 'db.query',
  EXTERNAL_API: 'http.request',
  CACHE_READ: 'cache.read',
  CACHE_WRITE: 'cache.write',
  RENDER_COMPONENT: 'component.render'
} as const

export async function trace<T>(
  spanName: string,
  fn: () => Promise<T>,
  attributes?: Record<string, string>
): Promise<T> {
  // إذا كان OpenTelemetry غير متاح، التنفيذ مباشرة
  if (typeof (globalThis as any).performance === 'undefined') {
    return fn()
  }

  const start = performance.now()
  
  try {
    const result = await fn()
    const duration = performance.now() - start
    
    // التسجيل في نظام السجلات
    if (duration > 100) {
      console.warn(
        `[TRACE] ${spanName} completed in ${duration.toFixed(2)}ms`,
        { attributes, duration }
      )
    }
    
    return result
  } catch (error) {
    const duration = performance.now() - start
    console.error(`[TRACE] ${spanName} failed after ${duration.toFixed(2)}ms`, error)
    throw error
  }
}

(2) الاستخدام في Server Action

TS
// src/actions/task.ts
'use server'

import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'

export async function createTask(formData: FormData) {
  return trace('server-action.createTask', async () => {
    const title = formData.get('title') as string
    
    await trace('db.query.create-task', () =>
      prisma.task.create({
        data: { title, projectId: formData.get('projectId') as string, status: 'TODO' }
      })
    )
    
    revalidatePath('/projects')
    return { success: true }
  })
}

▶ مثال: تتبع مكون الخادم

Output:

TEXT 📖 للعرض فقط
Server action executes and calls revalidatePath() to refresh the page cache.
TSX
// src/app/projects/page.tsx
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'

async function getProjects() {
  return trace('db.query.list-projects', async () => {
    const projects = await prisma.project.findMany({
      include: { _count: { select: { tasks: true } } },
      orderBy: { updatedAt: 'desc' },
      take: 50
    })
    return projects
  })
}

export default async function ProjectsPage() {
  const start = performance.now()
  
  const projects = await getProjects()
  const fetchTime = performance.now() - start
  
  return (
    <div>
      <p data-testid="fetch-time">
        Data fetched in {fetchTime.toFixed(0)}ms ({projects.length} projects)
      </p>
      {/* render projects... */}
    </div>
  )
}

Output:

TEXT 📖 للعرض فقط
Renders the ▶ Example: Server Component Tracing component UI as described in the section.


8. تحديد المسارات البطيئة وتحسينها

(1) وسيط تسجيل المسارات البطيئة

TS
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const start = Date.now()
  
  const response = NextResponse.next()
  
  // تسجيل الوقت المستغرق بعد اكتمال الاستجابة
  response.headers.set('X-Response-Time', '0')
  
  // استخدام AsyncLocalStorage أو أحداث مخصصة
  process.nextTick(() => {
    const duration = Date.now() - start
    
    // تسجيل الطلبات البطيئة
    if (duration > 1000) {
      console.warn(
        `[SLOW ROUTE] ${request.method} ${request.nextUrl.pathname} ` +
        `took ${duration}ms`
      )
    }
  })
  
  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}

(2) جدول تحليل المسارات البطيئة

المسار متوسط وقت الاستجابة P95 حجم الطلبات استراتيجية التحسين
/dashboard 1,234 ms 3,567 ms 10,000/يوم PPR static shell + تقسيم Suspense
/reports 2,891 ms 5,200 ms 500/يوم إضافة ISR revalidate=300، تخزين التقارير مؤقتًا
/api/search 1,567 ms 4,100 ms 8,000/يوم إضافة Redis caching والترقيم
/projects/[id]/tasks 892 ms 2,100 ms 6,000/يوم إضافة فهارس قاعدة بيانات، تحديد استعلامات N+1

(3) قائمة فحص تحسين الأداء

TSX
// 1. الاستيراد الديناميكي للمكونات الثقيلة
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="animate-pulse h-64" />,
  ssr: false  // تعطيل SSR للمكونات التي لا تحتاج SEO
})

// 2. تحسين الصور
import Image from 'next/image'
<Image
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  priority  // إضافة أولوية للصورة المميزة
  placeholder="blur"
  blurDataURL="data:image/webp;base64,..."
/>

// 3. جلب البيانات المتوازي
const [projects, tasks, members] = await Promise.all([
  getProjects(),
  getTasks(),
  getMembers()
])

// 4. إضافة رؤوس التخزين المؤقت
export const revalidate = 300  // ISR 5 دقائق
export const dynamic = 'force-static'

// 5. ضغط الاستجابة
// next.config.js
compress: true

// 6. الاتصال المسبق بمصادر الطرف الثالث
<link rel="preconnect" href="https://api.posthog.com" />


9. مثال كامل: لوحة تحكم مراقبة الأداء

TSX
// src/app/performance/page.tsx
// ============================================
// لوحة تحكم مراقبة الأداء
// ============================================
import { prisma } from '@/lib/prisma'

// --- 1. جمع البيانات ---
async function getPerformanceData() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  
  const weekAgo = new Date(today)
  weekAgo.setDate(weekAgo.getDate() - 7)

  // مؤشرات اليوم
  const todayVitals = await prisma.webVital.groupBy({
    by: ['metricName'],
    where: {
      timestamp: { gte: today },
      metricName: { in: ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] }
    },
    _avg: { metricValue: true },
    _count: true
  })

  // أفضل 10 مسارات بطيئة
  const slowRoutes = await prisma.webVital.groupBy({
    by: ['url'],
    where: {
      timestamp: { gte: weekAgo },
      metricRating: 'poor'
    },
    _count: true,
    _avg: { metricValue: true },
    orderBy: { _avg: { metricValue: 'desc' } },
    take: 10
  })

  // توزيع الأجهزة
  const deviceStats = await prisma.webVital.groupBy({
    by: ['deviceType'],
    where: { timestamp: { gte: today } },
    _count: true
  })

  // الاتجاهات اليومية
  const dailyTrend = await prisma.$queryRaw`
    SELECT
      DATE(timestamp) as day,
      AVG(CASE WHEN metric_name = 'LCP' THEN metric_value END) as avg_lcp,
      AVG(CASE WHEN metric_name = 'CLS' THEN metric_value END) as avg_cls
    FROM web_vitals
    WHERE timestamp >= ${weekAgo}
    GROUP BY DATE(timestamp)
    ORDER BY day ASC
  `

  return { todayVitals, slowRoutes, deviceStats, dailyTrend }
}

// --- 2. مكون بطاقة الأداء ---
function MetricCard({ name, value, rating, count }: {
  name: string; value: number; rating: string; count: number
}) {
  const colorMap = {
    good: 'text-green-600',
    'needs-improvement': 'text-yellow-600',
    poor: 'text-red-600'
  }

  const formatValue = (metric: string, val: number) => {
    if (metric === 'CLS') return val.toFixed(2)
    return `${val.toFixed(0)}ms`
  }

  return (
    <div className="bg-white rounded-lg shadow p-4">
      <h3 className="text-sm font-medium text-gray-500">{name}</h3>
      <p className={`text-2xl font-bold ${colorMap[rating as keyof typeof colorMap] || ''}`}>
        {formatValue(name, value)}
      </p>
      <p className="text-xs text-gray-400">{count} samples today</p>
    </div>
  )
}

// --- 3. مكونات الصفحة ---
export default async function PerformancePage() {
  const data = await getPerformanceData()

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-2xl font-bold">Performance Dashboard</h1>

      {/* Today CWV */}
      <div className="grid grid-cols-5 gap-4">
        {data.todayVitals.map(v => (
          <MetricCard
            key={v.metricName}
            name={v.metricName}
            value={v._avg.metricValue || 0}
            rating={
              (v._avg.metricValue || 0) < 2500 ? 'good' :
              (v._avg.metricValue || 0) < 4000 ? 'needs-improvement' : 'poor'
            }
            count={v._count}
          />
        ))}
      </div>

      {/* المسارات البطيئة */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Slow Routes (Last 7 Days)</h2>
        <table className="w-full text-sm">
          <thead>
            <tr className="text-left text-gray-500">
              <th className="pb-2">Route</th>
              <th className="pb-2">Avg Response</th>
              <th className="pb-2">Poor Samples</th>
            </tr>
          </thead>
          <tbody>
            {data.slowRoutes.map(r => (
              <tr key={r.url} className="border-t">
                <td className="py-2 font-mono">{r.url}</td>
                <td className="py-2">{r._avg.metricValue?.toFixed(0)}ms</td>
                <td className="py-2 text-red-600">{r._count}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* توزيع الأجهزة */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Device Distribution</h2>
        <div className="flex gap-8">
          {data.deviceStats.map(d => (
            <div key={d.deviceType}>
              <span className="text-2xl font-bold">{d._count}</span>
              <span className="text-gray-500 ml-2">{d.deviceType}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

❓ أسئلة شائعة

س ما الفرق بين Lighthouse CI و PageSpeed Insights؟
ج Lighthouse CI يعمل تلقائيًا داخل خط أنابيب CI/CD ويسمح لك بتعيين حدود لمنع تدهور الأداء. PageSpeed Insights هي أداة عبر الإنترنت تقدمها Google تعتمد على بيانات CrUX الحقيقية للمستخدمين. الاثنان يكملان بعضهما: Lighthouse CI يُستخدم للفحص قبل النشر، بينما PageSpeed Insights يُستخدم للتحقق بعد النشر.
س ماذا تعني LCP و INP و CLS في Core Web Vitals؟
ج LCP (Largest Contentful Paint) يقيس وقت عرض أكبر عنصر محتوى ويجب أن يكون < 2.5 ثانية. INP (Interaction to Next Paint) يقيس وقت استجابة الصفحة لتفاعلات المستخدم ويجب أن يكون < 200 ms. CLS (Cumulative Layout Shift) يقيس استقرار تخطيط الصفحة ويجب أن يكون < 0.1. هذه المقاييس الثلاثة تؤثر مباشرة على ترتيب بحث Google.
س ما الفرق بين "gzip" و "parsed" في تقرير Bundle Analyzer؟
ج "gzip" يشير إلى حجم الملف بعد ضغطه من قبل الخادم ونقله (حجم النقل الفعلي عبر الشبكة)، بينما "parsed" يشير إلى الحجم بعد فك ضغط المتصفح وتحليل الملف (مما يؤثر على وقت معالجة محرك JS). عادةً، يجب التركيز على حجم gzip (عنق زجاجة نقل الشبكة) وحجم parsed (عنق زجاجة معالجة CPU). يقوم Next.js بتمكين ضغط gzip افتراضيًا.
س هل يتعارض useReportWebVitals مع @vercel/speed-insights؟
ج لا، لا يتعارضان. useReportWebVitals هو callback مدمج في Next.js لـ Web Vitals يمكنك استخدامه للإبلاغ عن البيانات إلى خدمة التحليلات الخاصة بك. @vercel/speed-insights هي خدمة مدفوعة تقدمها Vercel تجمع وتعرض بيانات RUM تلقائيًا. يمكن استخدامهما معًا دون تداخل.
س ما الفرق بين /instrumentation.ts و middleware.ts؟
ج instrumentation.ts يعمل مرة واحدة عند بدء التطبيق (عند بدء الخادم) ويُستخدم لتسجيل التتبعات العامة مثل OpenTelemetry و Sentry. middleware.ts يعمل قبل كل طلب ويُستخدم لاعتراض الطلبات وإعادة التوجيه وحقن الرؤوس. لتتبع الأداء، نوصي باستخدام instrumentation.ts (عالمي)، ولفه في Server Action (منطق الأعمال)، وmiddleware (مستوى الطلب).
س من أين يجب أن أبدأ تحسين المسارات البطيئة؟
ج خطوات التحسين الموصى بها: (1) استخدم Bundle Analyzer لتحديد وتقسيم الحزم الكبيرة؛ (2) عيّن حواجز أداء باستخدام Lighthouse CI؛ (3) اجمع بيانات RUM لتحديد أبطأ 5% من المستخدمين؛ (4) ابدأ بتحسين أبطأ المسارات ذات أعلى حجم طلبات (باستخدام PPR و ISR والاستيراد الديناميكي)؛ (5) حسّن استعلامات قاعدة البيانات (استعلامات N+1، الفهارس المفقودة).

📖 ملخص


📝 تمارين

  1. سؤال أساسي (⭐): قم بإعداد lighthouserc.js لتدقيق الصفحة الرئيسية وصفحة تسجيل الدخول، وحدد ميزانيات أداء LCP < 2.5s و CLS < 0.1، وشغّل lhci autorun محليًا للتحقق من نجاح الإعداد.

  2. تمرين متقدم (⭐⭐): قم بدمج useReportWebVitals في تطبيقك: (1) أنشئ مسار API للإبلاغ عن البيانات وخزنها في قاعدة البيانات؛ (2) أضف مكون WebVitals إلى التخطيط الجذري؛ (3) أنشئ صفحة /performance لعرض مقاييس CWV لليوم وأفضل 10 مسارات بطيئة.

  3. تحدي (⭐⭐⭐): نفذ لوحة تحكم مراقبة أداء شاملة: (1) استخدم Bundle Analyzer لتحليل حجم الحزمة الحالي وقلل JavaScript الشاشة الأولى بأكثر من 40% عبر التحميل الديناميكي؛ (2) قم بإعداد حاجز Lighthouse CI في GitHub Actions لمنع دمج طلبات السحب (PRs) إذا انخفضت درجة الأداء بأكثر من 5 نقاط؛ (3) ادمج التتبع التلقائي capture_performance من PostHog وأنشئ مخطط اتجاه CWV لمدة 7 أيام.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%