
Web Performance in 2024: Core Web Vitals and Beyond
# Web Performance in 2024: Core Web Vitals and Beyond
Web performance has never been more critical. With Google's Page Experience update and the introduction of Interaction to Next Paint (INP), understanding and optimizing for Core Web Vitals is essential for both user experience and search rankings.
## The Evolution of Core Web Vitals
### Current Core Web Vitals (2024)
1. **Largest Contentful Paint (LCP)**: Loading performance
- Good: ≤ 2.5 seconds
- Needs Improvement: 2.5-4.0 seconds
- Poor: > 4.0 seconds
2. **Interaction to Next Paint (INP)**: Responsiveness (replaced FID in March 2024)
- Good: ≤ 200 milliseconds
- Needs Improvement: 200-500 milliseconds
- Poor: > 500 milliseconds
3. **Cumulative Layout Shift (CLS)**: Visual stability
- Good: ≤ 0.1
- Needs Improvement: 0.1-0.25
- Poor: > 0.25
### New Metrics to Watch
- **Time to First Byte (TTFB)**: Server response time
- **First Contentful Paint (FCP)**: First content render
- **Speed Index**: Visual completeness over time
- **Total Blocking Time (TBT)**: Main thread blocking time
## Optimizing Largest Contentful Paint (LCP)
### Identifying LCP Elements
```typescript
// Monitor LCP in production
import { onLCP } from 'web-vitals'
onLCP((metric) => {
// Send to analytics
gtag('event', 'web_vitals', {
event_category: 'Web Vitals',
event_action: 'LCP',
value: Math.round(metric.value),
custom_parameter_1: metric.id,
})
// Log LCP element for debugging
console.log('LCP element:', metric.entries[0]?.element)
})
```
### LCP Optimization Strategies
#### 1. Optimize Images
```typescript
// Next.js Image optimization
import Image from 'next/image'
function HeroSection() {
return (
alt="Hero image"
width={1200}
height={600}
priority // Preload above-the-fold images
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}
```
#### 2. Preload Critical Resources
```typescript
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{/* Preload critical fonts */}
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* Preload critical images */}
rel="preload"
href="/hero-image.jpg"
as="image"
/>
{/* Preconnect to external domains */}
{children}
)
}
```
#### 3. Optimize Server Response Time
```typescript
// Edge runtime for faster responses
export const runtime = 'edge'
export async function GET(request: Request) {
// Use edge-compatible database
const data = await getDataFromEdgeDB()
return Response.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
}
})
}
```
## Optimizing Interaction to Next Paint (INP)
### Understanding INP
INP measures the latency of all user interactions throughout the page lifecycle:
```typescript
// Monitor INP
import { onINP } from 'web-vitals'
onINP((metric) => {
console.log('INP:', metric.value)
// Get interaction details
const interaction = metric.entries[0]
console.log('Interaction type:', interaction.name)
console.log('Target element:', interaction.target)
console.log('Processing time:', interaction.processingEnd - interaction.processingStart)
})
```
### INP Optimization Techniques
#### 1. Optimize Event Handlers
```typescript
// Bad: Heavy computation in event handler
function BadButton() {
const handleClick = () => {
// Heavy synchronous work blocks the main thread
const result = heavyComputation()
updateUI(result)
}
return
}
// Good: Use scheduler to break up work
import { unstable_scheduleCallback, unstable_NormalPriority } from 'scheduler'
function GoodButton() {
const handleClick = () => {
// Schedule work to avoid blocking
unstable_scheduleCallback(unstable_NormalPriority, () => {
const result = heavyComputation()
updateUI(result)
})
}
return
}
```
#### 2. Use Web Workers for Heavy Tasks
```typescript
// worker.ts
self.onmessage = function(e) {
const { data, operation } = e.data
let result
switch (operation) {
case 'processData':
result = processLargeDataset(data)
break
case 'calculateMetrics':
result = calculateComplexMetrics(data)
break
}
self.postMessage(result)
}
// Component using Web Worker
function DataProcessor() {
const [result, setResult] = useState(null)
const workerRef = useRef
useEffect(() => {
workerRef.current = new Worker('/worker.js')
workerRef.current.onmessage = (e) => {
setResult(e.data)
}
return () => workerRef.current?.terminate()
}, [])
const processData = (data: any[]) => {
workerRef.current?.postMessage({
data,
operation: 'processData'
})
}
return (
{result &&
)
}
```
#### 3. Optimize React Rendering
```typescript
// Use React.memo and useMemo strategically
const ExpensiveComponent = React.memo(function ExpensiveComponent({
data,
onUpdate
}: {
data: ComplexData[]
onUpdate: (id: string) => void
}) {
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computed: expensiveComputation(item)
}))
}, [data])
const memoizedCallback = useCallback((id: string) => {
onUpdate(id)
}, [onUpdate])
return (
{processedData.map(item => (
data={item}
onUpdate={memoizedCallback}
/>
))}
)
})
```
## Optimizing Cumulative Layout Shift (CLS)
### Common CLS Causes and Solutions
#### 1. Images Without Dimensions
```typescript
// Bad: No dimensions specified

// Good: Specify dimensions
alt="Description"
width={400}
height={300}
style={{ aspectRatio: '4/3' }}
/>
// Better: Use Next.js Image with aspect ratio
alt="Description"
width={400}
height={300}
style={{ aspectRatio: '4/3' }}
/>
```
#### 2. Dynamic Content Insertion
```typescript
// Reserve space for dynamic content
function AdBanner() {
const [adLoaded, setAdLoaded] = useState(false)
return (
className="ad-container"
style={{
minHeight: adLoaded ? 'auto' : '250px',
backgroundColor: adLoaded ? 'transparent' : '#f0f0f0'
}}
>
{adLoaded ? (
) : (
)}
)
}
```
#### 3. Font Loading Optimization
```css
/* Prevent layout shift during font loading */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap; /* or 'fallback' for better CLS */
font-weight: 100 900;
}
/* Size-adjust for better font fallback matching */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%; /* Adjust to match Inter's metrics */
}
body {
font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}
```
## Advanced Performance Techniques
### Resource Hints and Preloading
```typescript
// Strategic resource hints
function OptimizedPage() {
return (
<>
{/* DNS prefetch for external domains */}
{/* Preconnect for critical third-parties */}
{/* Module preload for critical JavaScript */}
{/* Prefetch for likely next navigation */}
{/* Page content */}
>
)
}
```
### Service Worker for Performance
```typescript
// sw.js - Service Worker for caching
const CACHE_NAME = 'app-v1'
const STATIC_ASSETS = [
'/',
'/styles.css',
'/app.js',
'/fonts/inter-var.woff2'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request)
})
)
})
```
### Performance Monitoring
```typescript
// Comprehensive performance monitoring
import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals'
function sendToAnalytics(metric: any) {
// Send to your analytics service
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
url: window.location.href,
userAgent: navigator.userAgent,
}),
})
}
// Monitor all Core Web Vitals
onCLS(sendToAnalytics)
onFCP(sendToAnalytics)
onLCP(sendToAnalytics)
onTTFB(sendToAnalytics)
// Monitor INP (replaces FID)
import { onINP } from 'web-vitals'
onINP(sendToAnalytics)
```
## Performance Testing and Monitoring
### Automated Performance Testing
```typescript
// Lighthouse CI configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/about'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
}
```
### Real User Monitoring (RUM)
```typescript
// RUM implementation
class PerformanceMonitor {
private observer: PerformanceObserver
constructor() {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.handlePerformanceEntry(entry)
}
})
// Observe different entry types
this.observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] })
}
private handlePerformanceEntry(entry: PerformanceEntry) {
switch (entry.entryType) {
case 'navigation':
this.trackNavigation(entry as PerformanceNavigationTiming)
break
case 'resource':
this.trackResource(entry as PerformanceResourceTiming)
break
case 'paint':
this.trackPaint(entry as PerformancePaintTiming)
break
}
}
private trackNavigation(entry: PerformanceNavigationTiming) {
const metrics = {
ttfb: entry.responseStart - entry.requestStart,
domContentLoaded: entry.domContentLoadedEventEnd - entry.navigationStart,
loadComplete: entry.loadEventEnd - entry.navigationStart,
}
this.sendMetrics('navigation', metrics)
}
private sendMetrics(type: string, metrics: any) {
// Send to your monitoring service
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify({ type, metrics, timestamp: Date.now() }),
})
}
}
// Initialize monitoring
new PerformanceMonitor()
```
## Performance Budget and Monitoring
### Setting Performance Budgets
```json
// performance-budget.json
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 400
},
{
"resourceType": "total",
"budget": 2000
}
],
"resourceCounts": [
{
"resourceType": "third-party",
"budget": 10
}
],
"timings": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1500
}
]
}
```
## Conclusion
Web performance in 2024 requires a holistic approach that goes beyond traditional metrics. With the introduction of INP and evolving Core Web Vitals, developers must focus on:
1. **Loading Performance**: Optimize LCP through image optimization, resource preloading, and server response times
2. **Interactivity**: Improve INP by optimizing event handlers, using Web Workers, and managing main thread blocking
3. **Visual Stability**: Prevent CLS through proper sizing, font optimization, and reserved space for dynamic content
4. **Continuous Monitoring**: Implement both synthetic and real user monitoring to track performance over time
The key to success is measuring, optimizing, and monitoring continuously. Performance is not a one-time task but an ongoing commitment to providing the best possible user experience.
Remember: every millisecond matters. Users notice performance improvements, and search engines reward fast websites. Invest in performance optimization—it's one of the best investments you can make for your web applications.
As web technologies continue to evolve, staying current with performance best practices and new optimization techniques will be crucial for maintaining competitive, user-friendly websites that rank well in search results and provide exceptional user experiences.



