Next.js: تحليل ومراقبة الأداء
آخر تحديث: 2026-08-26
الأداء هو سمة أساسية — في عصر الإنترنت حيث يمكن أن يؤدي تأخير 100 مللي ثانية إلى فقدان 7% من المستخدمين، يُعد تحسين الأداء مقياسًا جوهريًا للقدرة التنافسية للمنتج.
1. ما ستتعلمه
- دمج Lighthouse CI وتعيين حدود الأداء (lighthouserc.js)
- استخدام @next/bundle-analyzer لتصور حجم حزمة JavaScript
- جمع بيانات Core Web Vitals من المستخدمين الحقيقيين باستخدام مكتبة Web Vitals
- دمج @vercel/speed-insights و PostHog لتتبع الأداء
- تخصيص تتبعات OpenTelemetry في /instrumentation.ts
- تحديد المسارات البطيئة وتحسينها
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 | تحميل بطيء للشاشة الأولى |
والأسوأ من ذلك، أنها لم تستطع الإجابة عن الأسئلة التالية:
- هل ارتفع LCP في بيئة الإنتاج أم انخفض خلال الأسبوع الماضي؟
- ما هي الصفحات الأكثر بطئًا؟
- هل يُدخل الكود الذي تم إصداره حديثًا أي تدهور في الأداء؟
(2) حلول لأنظمة مراقبة الأداء
أنشأت ديانا نظام مراقبة أداء شامل:
// 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 ويحدد حدود ميزانية الأداء.
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
// 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
# .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
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 المضغوطة بصريًا لمساعدتك في تحديد التبعيات كبيرة الحجم.
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
// 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) التحليل التشغيلي
{
"scripts": {
"analyze": "ANALYZE=true npm run build",
"analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
}
}
▶ مثال: تفسير تقرير تحليل الحزمة
ANALYZE=true npm run build
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-fns ← dayjs (45KB ← 6KB) |
-39 KB |
| التحميل الكسول | React.lazy + Suspense |
-85 KB مكتبة تفاعل |
5. مراقبة Core Web Vitals للمستخدمين الحقيقيين
توفر Lighthouse بيانات معملية؛ فقط مراقبة المستخدم الحقيقي (RUM) يمكنها أن تعكس تجربة المستخدم الفعلية.
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
// 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
// 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) استعلام لوحة تحكم الأداء
// 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
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) الدمج من جانب العميل
// 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) الدمج في التخطيط الجذري
// 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 لمراقبة أداء جانب الخادم والمسارات البطيئة.
// 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 مخصص
// 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
// 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:
Server action executes and calls revalidatePath() to refresh the page cache.
// 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:
Renders the ▶ Example: Server Component Tracing component UI as described in the section.
8. تحديد المسارات البطيئة وتحسينها
(1) وسيط تسجيل المسارات البطيئة
// 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) قائمة فحص تحسين الأداء
// 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. مثال كامل: لوحة تحكم مراقبة الأداء
// 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>
)
}
❓ أسئلة شائعة
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 (مستوى الطلب).📖 ملخص
- Lighthouse CI يدقق تلقائيًا في الأداء وإمكانية الوصول وأفضل الممارسات في خطوط أنابيب CI/CD، باستخدام حواجز قائمة على الحدود لمنع دمج الكود المتدهور
@next/bundle-analyzerيعرض أحجام الحزم بصريًا، ويحدد فشل tree-shaking والتبعيات كبيرة الحجم- Core Web Vitals RUM تجمع بيانات المستخدمين الحقيقيين (LCP/INP/CLS) عبر
useReportWebVitalsوتُبلغ عنها إلى خدمة التحليلات - PostHog و
@vercel/speed-insightsتوفر لوحات تحكم جاهزة لتحليل الأداء /instrumentation.tsتسجل تتبعات OpenTelemetry وتستخدم دوالtrace()مخصصة لمراقبة أداء الخادم- التحسين بناءً على الأولوية بعد تحديد المسارات البطيئة: PPR static shells، تخزين ISR المؤقت، الاستيراد الديناميكي، وجلب البيانات المتوازي
📝 تمارين
-
سؤال أساسي (⭐): قم بإعداد
lighthouserc.jsلتدقيق الصفحة الرئيسية وصفحة تسجيل الدخول، وحدد ميزانيات أداء LCP < 2.5s و CLS < 0.1، وشغّلlhci autorunمحليًا للتحقق من نجاح الإعداد. -
تمرين متقدم (⭐⭐): قم بدمج
useReportWebVitalsفي تطبيقك: (1) أنشئ مسار API للإبلاغ عن البيانات وخزنها في قاعدة البيانات؛ (2) أضف مكونWebVitalsإلى التخطيط الجذري؛ (3) أنشئ صفحة/performanceلعرض مقاييس CWV لليوم وأفضل 10 مسارات بطيئة. -
تحدي (⭐⭐⭐): نفذ لوحة تحكم مراقبة أداء شاملة: (1) استخدم Bundle Analyzer لتحليل حجم الحزمة الحالي وقلل JavaScript الشاشة الأولى بأكثر من 40% عبر التحميل الديناميكي؛ (2) قم بإعداد حاجز Lighthouse CI في GitHub Actions لمنع دمج طلبات السحب (PRs) إذا انخفضت درجة الأداء بأكثر من 5 نقاط؛ (3) ادمج التتبع التلقائي
capture_performanceمن PostHog وأنشئ مخطط اتجاه CWV لمدة 7 أيام.