404 Not Found

404 Not Found


nginx

Performance Analysis and Monitoring: From Lighthouse to Core Web Vitals

2. A True Story of a Performance Engineer

(1) Pain Point: Lighthouse score of 45; users are leaving

Diana is a performance engineer on the TaskFlow platform. She recently received a user behavior analysis report:

Metric Current Value Industry Benchmark Impact
LCP 4.8s < 2.5s User bounce rate 45%
FID/INP 320 ms < 200 ms Interaction feels choppy
CLS 0.35 < 0.1 Misclicks caused by layout shifts
Bundle Size 1.2 MB < 500 KB Slow first-screen load

To make matters worse, she couldn't answer the following questions:

(2) Solutions for Performance Monitoring Systems

Diana has established a comprehensive performance monitoring system:

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) Revenue

Dimension Before Optimization After Optimization
LCP 4.8s 1.2s
Bundle Size 1.2 MB 380 KB
Lighthouse Score 45/100 92/100
Performance Degradation Detection Discovered after deployment Intercepted during the PR phase

3. Lighthouse CI

Lighthouse CI automatically runs Lighthouse audits in the CI/CD pipeline and sets performance budget thresholds.

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 Configuration

JS
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // Number of times collected(Find the median)
      numberOfRuns: 3,
      
      // Pages to Be Audited
      url: [
        'http://localhost:3000',
        'http://localhost:3000/login',
        'http://localhost:3000/dashboard',
        'http://localhost:3000/projects',
        'http://localhost:3000/projects/p1'
      ],
      
      // Start Next.js Development Server
      startServerCommand: 'npm run start -p 3000',
      startServerReadyPattern: 'ready started server',
      
      // Equipment Simulation
      settings: {
        preset: 'desktop',
        throttling: {
          cpuSlowdownMultiplier: 4,
          downloadThroughputKbps: 10000,
          uploadThroughputKbps: 5000,
          rttMs: 40
        }
      }
    },
    
    assert: {
      // Performance-Based Access Control
      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 Score Threshold
      assertions: {
        // Category Score
        '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 }],
        
        // Other Key Metrics
        'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
        'interactive': ['warn', { maxNumericValue: 3500 }],
        'max-potential-fid': ['warn', { maxNumericValue: 100 }],
        
        // Best Practices
        '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: {
      // Allow external links(CI Environment)
      allowStaticServer: true
    }
  }
}

(2) CI Integration

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 }}

▶ Example: Interpreting Lighthouse CI Reports

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 Visualizes the size of minified JavaScript modules to help you identify dependencies that are too large.

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) Configuring 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) Operational Analysis

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

▶ Example: Interpreting the Bundle Analysis Report

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) Bundle Optimization Strategies

Strategy Method Expected Savings
Dynamic Import const Chart = dynamic(() => import('./Chart')) -280 KB Initial Load
Tree Shaking Use ESM imports instead of CJS -20% dead code
Code Splitting Automatic splitting by route (Next.js default) -30% JavaScript on first screen
Dependency Replacement date-fnsdayjs (45KB → 6KB) -39 KB
Lazy Loading React.lazy + Suspense -85 KB Interaction Library

5. Core Web Vitals Real-User Monitoring

Lighthouse provides lab data; only Real User Monitoring (RUM) can reflect the actual user experience.

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 Reporting Component

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) => {
    // Report to Analytics Services
    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()
    }

    // Usage sendBeacon Ensure that data is reported even when the page is unloaded
    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) Vitals API Route (Data Storage)

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()
    
    // Save to the database
    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 the indicator is poor,Trigger an alert
    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) {
  // Looking Back 5 Within minutes, the path's poor Number of Indicators
  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 it exceeds the threshold,Send an Alert
  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) Performance Dashboard Query

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
  })
  
  // Calculation good / needs-improvement / poor Ratio
  const ratings = await prisma.webVital.groupBy({
    by: ['metricRating'],
    where: {
      timestamp: { gte: today }
    },
    _count: true
  })
  
  return { vitals, ratings }
}

▶ Example: Interpreting Web Vitals Data

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 Integration

PostHog is an open-source product analytics platform that includes built-in session recording, feature flags, and Speed Insights.

(1) Client Integration

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,  // Automatic Capture 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) Root Layout Integration

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 Custom Tracing

Next.js 16 supports registering OpenTelemetry traces via instrumentation.ts to monitor server-side performance and slow routes.

TS
// src/instrumentation.ts
// ============================================
// Next.js 16 Custom Telemetry
// ============================================
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) Custom Span Tracking

TS
// src/lib/tracing.ts
// Custom Tracking 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> {
  // If OpenTelemetry Unavailable,Execute Directly
  if (typeof (globalThis as any).performance === 'undefined') {
    return fn()
  }

  const start = performance.now()
  
  try {
    const result = await fn()
    const duration = performance.now() - start
    
    // Recorded in the log system
    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) Use in 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 }
  })
}

▶ Example: Server Component Tracing

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>
  )
}

8. Identifying and Optimizing Slow Routes

(1) Slow-Route Logging Middleware

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()
  
  // Record the time taken after the response is complete
  response.headers.set('X-Response-Time', '0')
  
  // Usage AsyncLocalStorage or custom events
  process.nextTick(() => {
    const duration = Date.now() - start
    
    // Log Slow Requests
    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) Slow Route Analysis Table

Route Average Response Time P95 Request Volume Optimization Strategy
/dashboard 1,234 ms 3,567 ms 10,000/day PPR static shell + Suspense splitting
/reports 2,891 ms 5,200 ms 500/day Add ISR revalidate=300, cache reports
/api/search 1,567 ms 4,100 ms 8,000/day Add Redis caching and pagination
/projects/[id]/tasks 892 ms 2,100 ms 6,000/day Add database indexes to limit N+1 queries

(3) Performance Optimization Checklist

TSX
// 1. Dynamic Import of Heavyweight Components
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="animate-pulse h-64" />,
  ssr: false  // For those who don't need SEO Component disabled SSR
})

// 2. Image Optimization
import Image from 'next/image'
<Image
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  priority  // Add a Featured Image priority
  placeholder="blur"
  blurDataURL="data:image/webp;base64,..."
/>

// 3. Parallel Data Retrieval
const [projects, tasks, members] = await Promise.all([
  getProjects(),
  getTasks(),
  getMembers()
])

// 4. Add Cache Headers
export const revalidate = 300  // ISR 5 minutes
export const dynamic = 'force-static'

// 5. Compression Response
// next.config.js
compress: true

// 6. Pre-connect to Third-Party Sources
<link rel="preconnect" href="https://api.posthog.com" />

9. Complete Example: Performance Monitoring Dashboard

TSX
// src/app/performance/page.tsx
// ============================================
// Performance Monitoring Dashboard
// ============================================
import { prisma } from '@/lib/prisma'

// --- 1. Data Collection ---
async function getPerformanceData() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  
  const weekAgo = new Date(today)
  weekAgo.setDate(weekAgo.getDate() - 7)

  // Today's Indicators
  const todayVitals = await prisma.webVital.groupBy({
    by: ['metricName'],
    where: {
      timestamp: { gte: today },
      metricName: { in: ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] }
    },
    _avg: { metricValue: true },
    _count: true
  })

  // Slow Routing TOP 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
  })

  // Equipment Distribution
  const deviceStats = await prisma.webVital.groupBy({
    by: ['deviceType'],
    where: { timestamp: { gte: today } },
    _count: true
  })

  // Daily Trends
  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. Performance Card Component ---
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. Page Components ---
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>

      {/* Slow Routing */}
      <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>

      {/* Equipment Distribution */}
      <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>
  )
}

❓ FAQ

Q What is the difference between Lighthouse CI and PageSpeed Insights?
A Lighthouse CI runs automatically within a CI/CD pipeline and allows you to set thresholds to prevent performance degradation. PageSpeed Insights is an online tool provided by Google that is based on CrUX real-user data. The two complement each other: Lighthouse CI is used for pre-deployment checks, while PageSpeed Insights is used for post-deployment verification.
Q What do LCP, INP, and CLS in Core Web Vitals stand for?
A LCP (Largest Contentful Paint) measures the rendering time of the largest content element and should be < 2.5 seconds. INP (Interaction to Next Paint) measures the page’s response time to user interactions and should be < 200 ms. CLS (Cumulative Layout Shift) measures page layout stability and should be < 0.1. These three metrics directly impact Google search rankings.
Q What is the difference between “gzip” and “parsed” in the Bundle Analyzer report?
A “gzip” refers to the size of the file after it has been compressed by the server and transmitted (the actual network transfer size), while “parsed” refers to the size after the browser has decompressed and parsed the file (which affects the JS engine’s processing time). Typically, you should focus on the gzip size (network transfer bottleneck) and the parsed size (CPU processing bottleneck). Next.js enables gzip compression by default.
Q Do useReportWebVitals and @vercel/speed-insights conflict?
A No, they do not conflict. useReportWebVitals is a built-in Next.js Web Vitals callback that you can use to report data to your own analytics service. @vercel/speed-insights is a paid service provided by Vercel that automatically collects and displays RUM data. Both can be used simultaneously without interfering with each other.
Q What is the difference between /instrumentation.ts and middleware.ts?
A instrumentation.ts runs once when the application starts (when the server starts) and is used to register global telemetry such as OpenTelemetry and Sentry. middleware.ts runs before each request and is used for request interception, redirection, and header injection. For performance tracing, we recommend using instrumentation.ts (global), wrapped in a Server Action (business logic), and middleware (request-level).
Q Where should I start optimizing slow routes?
A Recommended optimization steps: (1) Use Bundle Analyzer to identify and split large bundles; (2) Set performance gatekeepers using Lighthouse CI; (3) Collect RUM data to identify the slowest 5% of users; (4) Start optimizing the slowest routes with the highest request volume (using PPR, ISR, and dynamic import); (5) Optimize database queries (N+1 queries, missing indexes).

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Configure lighthouserc.js to audit the homepage and login page, set performance budgets of LCP < 2.5s and CLS < 0.1, and run lhci autorun locally to verify that the configuration passes.

  2. Advanced Exercise (⭐⭐): Integrate useReportWebVitals into your application: (1) Create an API route for reporting data and store it in the database; (2) Add the WebVitals component to the root layout; (3) Create a /performance page to display today’s CWV metrics and the top 10 slow routes.

  3. Challenge (⭐⭐⭐): Implement a comprehensive performance monitoring dashboard: (1) Use Bundle Analyzer to analyze the current bundle size and reduce the first-screen JavaScript by more than 40% through dynamic loading; (2) Set up a Lighthouse CI GitHub Actions gate to block merging of pull requests (PRs) if their performance score drops by more than 5 points; (3) Integrate PostHog’s capture_performance automated tracking and create a 7-day CWV trend chart.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏