Performance Analysis & Monitoring
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:
- Has the LCP in the production environment gone up or down over the past week?
- Which pages are the slowest?
- Does the newly released code introduce any performance degradation?
(2) Solutions for Performance Monitoring Systems
Diana has established a comprehensive performance monitoring system:
// 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.
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
// 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
# .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
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.
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
// 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
{
"scripts": {
"analyze": "ANALYZE=true npm run build",
"analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
}
}
▶ Example: Interpreting the Bundle Analysis Report
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) 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-fns → dayjs (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.
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
// 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)
// 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
// 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
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
// 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
// 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.
// 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
// 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
// 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
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. Identifying and Optimizing Slow Routes
(1) Slow-Route Logging Middleware
// 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
// 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
// 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
useReportWebVitals and @vercel/speed-insights 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./instrumentation.ts and middleware.ts?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).📖 Summary
- Lighthouse CI automatically audits performance, accessibility, and best practices in CI/CD pipelines, using threshold-based gatekeeping to prevent the merging of degraded code
@next/bundle-analyzerVisualize package sizes, identify tree-shaking failures and oversized dependencies- Core Web Vitals RUM collects real-user data (LCP/INP/CLS) via
useReportWebVitalsand reports it to the analytics service - PostHog and
@vercel/speed-insightsoffer out-of-the-box performance analysis dashboards /instrumentation.tsRegister OpenTelemetry traces and use customtrace()functions to monitor server performance- Optimize based on priority after identifying slow routes: PPR static shells, ISR caching, dynamic import, and parallel data retrieval
📝 Exercises
-
Basic Question (⭐): Configure
lighthouserc.jsto audit the homepage and login page, set performance budgets of LCP < 2.5s and CLS < 0.1, and runlhci autorunlocally to verify that the configuration passes. -
Advanced Exercise (⭐⭐): Integrate
useReportWebVitalsinto your application: (1) Create an API route for reporting data and store it in the database; (2) Add theWebVitalscomponent to the root layout; (3) Create a/performancepage to display today’s CWV metrics and the top 10 slow routes. -
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_performanceautomated tracking and create a 7-day CWV trend chart.



