Web Performance Monitoring: The Complete Guide (2024)
Web Performance Monitoring: The Complete Guide (2024)
Use ScreenScout's Performance Monitoring to track and optimize your website's performance metrics in real-time!
Understanding Web Performance Monitoring
Web performance monitoring is the practice of measuring, analyzing, and optimizing the speed and efficiency of web applications. It's a critical aspect of web development that directly impacts user experience, business metrics, and search engine rankings.
The Business Case for Performance
Let's look at some real-world examples of how performance impacts business:
- Pinterest reduced their wait time by 40% and saw a 15% increase in SEO traffic
- BBC found they lost 10% of users for every additional second their site took to load
- Financial Times discovered that a 1-second delay in page load time led to a 4.9% drop in article views
- Mobify found that each 100ms improvement in homepage load speed resulted in a 1.11% increase in conversion
Studies show that 47% of users expect websites to load in under 2 seconds, and 40% will abandon a site that takes more than 3 seconds to load. This directly impacts your bottom line.
Key Performance Metrics Explained
Before diving into Core Web Vitals, it's essential to understand the fundamental metrics that affect web performance:
Time to First Byte (TTFB)
- What it measures: Time from request to first byte of response
- Good TTFB: < 200ms
- Affects: Initial rendering and perceived performance
- Common issues: Server configuration, network latency, DNS lookup time
First Contentful Paint (FCP)
- What it measures: Time until first content is painted
- Good FCP: < 1.8s
- Affects: User perception of page load
- Optimization tips: Minimize render-blocking resources, optimize server response
Time to Interactive (TTI)
- What it measures: Time until page becomes fully interactive
- Good TTI: < 3.8s
- Affects: User frustration, bounce rates
- Common bottlenecks: Heavy JavaScript, third-party scripts
Core Web Vitals Deep Dive
Core Web Vitals are Google's initiative to provide unified guidance for quality signals that are essential to delivering a great user experience on the web.
Largest Contentful Paint (LCP)
LCP measures loading performance by timing how long it takes to render the largest content element in the viewport.
type LCPMetric = {
value: number // Time in milliseconds
element: Element // The largest element
rating: 'good' | 'needs-improvement' | 'poor'
threshold: {
good: 2500, // Under 2.5s
poor: 4000 // Over 4s
}
}
What Affects LCP?
Server Response Time
- Optimize server configuration
- Use CDN for global reach
- Implement caching strategies
Resource Load Time
- Optimize images and fonts
- Minimize render-blocking resources
- Implement resource prioritization
Client-side Rendering
- Consider server-side rendering for critical content
- Implement progressive hydration
- Use streaming server-side rendering
Common elements that define LCP:
- Images (including background images)
- Video thumbnails
- Text blocks
- SVG elements
Focus your optimization efforts on these elements first.
First Input Delay (FID)
FID measures interactivity by timing how long it takes for the page to respond to the first user interaction.
type FIDMetric = {
value: number
rating: 'good' | 'needs-improvement' | 'poor'
threshold: {
good: 100, // Under 100ms
poor: 300 // Over 300ms
}
}
Understanding FID
What Causes Poor FID?
- Long-running JavaScript
- Large JavaScript bundles
- Heavy third-party scripts
- Complex component initialization
How to Measure FID
// Real-user FID measurement import { onFID } from 'web-vitals'; onFID(({ value, rating }) => { console.log(`FID: ${value}ms (${rating})`); // Common causes of poor FID performance.getEntriesByType('longtask').forEach(task => { if (task.duration > 50) { console.warn(`Long task detected: ${task.duration}ms`); } }); });
Optimization Strategies
- Break up long tasks
- Optimize JavaScript execution
- Implement code splitting
- Use web workers for heavy computation
Cumulative Layout Shift (CLS)
CLS measures visual stability by calculating how much unexpected layout shift occurs during the entire lifespan of the page.
type CLSMetric = {
value: number
rating: 'good' | 'needs-improvement' | 'poor'
threshold: {
good: 0.1, // Under 0.1
poor: 0.25 // Over 0.25
}
}
Understanding Layout Shifts
What Causes Layout Shifts?
- Images without dimensions
- Dynamically injected content
- Web fonts causing FOUT/FOIT
- Dynamic ads and embeds
How to Calculate CLS
// Layout shift score calculation function calculateLayoutShiftScore(impact, distance) { const impactFraction = impact; // Area of viewport affected const distanceFraction = distance / viewportHeight; return impactFraction * distanceFraction; } // Monitor layout shifts const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { const score = calculateLayoutShiftScore( entry.impactFraction, entry.value ); console.log(`Layout shift detected: ${score}`); } } }); observer.observe({ entryTypes: ['layout-shift'] });
Prevention Strategies
<!-- Always specify image dimensions --> <img src="hero.jpg" width="800" height="400" loading="lazy" style="aspect-ratio: 2/1" /> <!-- Reserve space for dynamic content --> <div style="min-height: 400px"> <!-- Dynamic content here --> </div> <!-- Preload critical fonts --> <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
Optimization Strategies: A Deep Dive
Performance optimization is an iterative process that requires a systematic approach. Let's explore detailed strategies for different aspects of web performance.
1. Resource Optimization
Image Optimization Strategies
Images often account for the largest portion of a webpage's size. Here's a comprehensive approach to image optimization:
Format Selection
interface ImageFormat { format: 'jpeg' | 'png' | 'webp' | 'avif' useCase: string[] compression: 'lossy' | 'lossless' browserSupport: string[] typical_savings: string } const formatGuide: Record<string, ImageFormat> = { webp: { format: 'webp', useCase: ['photographs', 'complex images'], compression: 'lossy', browserSupport: ['Chrome', 'Firefox', 'Edge', 'Safari 14+'], typical_savings: '25-35% vs JPEG' }, avif: { format: 'avif', useCase: ['photographs', 'complex images'], compression: 'lossy', browserSupport: ['Chrome', 'Firefox'], typical_savings: '40-50% vs JPEG' } }
Responsive Images Implementation
interface ImageBreakpoint { width: number height: number quality: number } class ResponsiveImageGenerator { private breakpoints: ImageBreakpoint[] = [ { width: 320, height: 240, quality: 70 }, { width: 640, height: 480, quality: 75 }, { width: 1024, height: 768, quality: 80 }, { width: 1920, height: 1080, quality: 85 } ] generateSrcSet(imagePath: string): string { return this.breakpoints .map(bp => { const url = this.generateImageUrl(imagePath, bp) return `${url} ${bp.width}w` }) .join(', ') } generateSizes(customSizes?: string): string { return customSizes || this.breakpoints .map(bp => `(max-width: ${bp.width}px) ${bp.width}px`) .join(', ') } private generateImageUrl( path: string, breakpoint: ImageBreakpoint ): string { // Implementation for your image service return `/image-service?path=${path}&w=${breakpoint.width}&q=${breakpoint.quality}` } }
Lazy Loading Implementation
class LazyLoader { private observer: IntersectionObserver private loadedImages: Set<string> = new Set() constructor() { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { rootMargin: '50px 0px', threshold: 0.01 } ) } observe(element: Element) { if (element.getAttribute('data-src')) { this.observer.observe(element) } } private async handleIntersection( entries: IntersectionObserverEntry[] ) { for (const entry of entries) { if (entry.isIntersecting) { const element = entry.target as HTMLImageElement const src = element.getAttribute('data-src') if (src && !this.loadedImages.has(src)) { await this.loadImage(element, src) this.loadedImages.add(src) this.observer.unobserve(element) } } } } private loadImage( img: HTMLImageElement, src: string ): Promise<void> { return new Promise((resolve, reject) => { img.onload = () => { img.classList.add('loaded') resolve() } img.onerror = reject img.src = src }) } }
2. JavaScript Performance Optimization
Code Splitting and Dynamic Imports
Modern JavaScript applications can quickly grow in size. Here's how to implement intelligent code splitting:
// Router-based code splitting
interface Route {
path: string
component: () => Promise<any>
preload?: boolean
}
class RouterConfig {
private routes: Route[] = [
{
path: '/',
component: () => import('./pages/Home'),
preload: true
},
{
path: '/dashboard',
component: () => import('./pages/Dashboard')
},
{
path: '/settings',
component: () => import('./pages/Settings')
}
]
constructor() {
this.setupPreloading()
}
private setupPreloading() {
// Preload critical routes
this.routes
.filter(route => route.preload)
.forEach(route => {
const link = document.createElement('link')
link.rel = 'modulepreload'
link.href = this.getModulePath(route.path)
document.head.appendChild(link)
})
// Setup intersection observer for other routes
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const route = this.routes.find(
r => r.path === entry.target.getAttribute('data-route')
)
if (route) {
route.component() // Trigger preload
}
}
})
},
{ rootMargin: '100px' }
)
// Observe route links
document.querySelectorAll('[data-route]').forEach(
el => observer.observe(el)
)
}
}
Memory Management
class MemoryManager {
private memoryThreshold = 0.9 // 90% of available memory
private gcThreshold = 0.8 // 80% of available memory
private observer: PerformanceObserver
constructor() {
this.setupMemoryMonitoring()
}
private setupMemoryMonitoring() {
this.observer = new PerformanceObserver((list) => {
const memory = list.getEntries()[0] as any
if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > this.memoryThreshold) {
this.handleHighMemoryUsage()
}
})
this.observer.observe({ entryTypes: ['memory'] })
}
private handleHighMemoryUsage() {
// Clear caches
this.clearImageCache()
this.clearResponseCache()
// Force garbage collection if available
if (window.gc) {
window.gc()
}
// Notify monitoring system
this.notifyMemoryPressure()
}
private clearImageCache() {
// Implementation
}
private clearResponseCache() {
caches.keys().then(names => {
names.forEach(name => {
caches.delete(name)
})
})
}
}
3. Network Optimization
Caching Strategies
interface CacheConfig {
strategy: 'network-first' | 'cache-first' | 'stale-while-revalidate'
maxAge: number
version: string
}
class CacheManager {
private config: Record<string, CacheConfig> = {
static: {
strategy: 'cache-first',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
version: 'v1'
},
api: {
strategy: 'stale-while-revalidate',
maxAge: 60 * 1000, // 1 minute
version: 'v1'
}
}
async fetch(request: Request): Promise<Response> {
const cacheKey = this.getCacheKey(request)
const config = this.getConfigForRequest(request)
switch (config.strategy) {
case 'network-first':
return this.networkFirst(request, cacheKey, config)
case 'cache-first':
return this.cacheFirst(request, cacheKey, config)
case 'stale-while-revalidate':
return this.staleWhileRevalidate(request, cacheKey, config)
default:
return fetch(request)
}
}
private async networkFirst(
request: Request,
cacheKey: string,
config: CacheConfig
): Promise<Response> {
try {
const response = await fetch(request)
const cache = await caches.open(config.version)
cache.put(cacheKey, response.clone())
return response
} catch (error) {
const cachedResponse = await caches.match(cacheKey)
if (cachedResponse) return cachedResponse
throw error
}
}
private async cacheFirst(
request: Request,
cacheKey: string,
config: CacheConfig
): Promise<Response> {
const cachedResponse = await caches.match(cacheKey)
if (cachedResponse) {
// Validate age
const age = this.getResponseAge(cachedResponse)
if (age < config.maxAge) {
return cachedResponse
}
}
const response = await fetch(request)
const cache = await caches.open(config.version)
cache.put(cacheKey, response.clone())
return response
}
private async staleWhileRevalidate(
request: Request,
cacheKey: string,
config: CacheConfig
): Promise<Response> {
const cachedResponse = await caches.match(cacheKey)
const networkPromise = fetch(request).then(response => {
const cache = await caches.open(config.version)
cache.put(cacheKey, response.clone())
return response
})
return cachedResponse || networkPromise
}
}
Implementation Guide
Setting Up Performance Monitoring
// Using web-vitals library
import {onCLS, onFID, onLCP} from 'web-vitals';
function sendToAnalytics({name, value, rating}) {
const body = JSON.stringify({name, value, rating});
// Use Navigator.sendBeacon() for better reliability
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
} else {
fetch('/analytics', {
method: 'POST',
body,
keepalive: true
});
}
}
// Monitor Core Web Vitals
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
Real User Monitoring (RUM)
interface PerformanceData {
navigation: {
type: string
timing: PerformanceTiming
}
resources: PerformanceResourceTiming[]
paint: PerformancePaintTiming[]
}
function collectPerformanceData(): PerformanceData {
return {
navigation: performance.getEntriesByType('navigation')[0],
resources: performance.getEntriesByType('resource'),
paint: performance.getEntriesByType('paint')
}
}
// Monitor resource loading
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
analyzeFetchPerformance(entry);
}
}
});
observer.observe({ entryTypes: ['resource'] });
Implementation Guide: From Theory to Practice
Understanding performance metrics is just the first step. Let's dive into how to implement comprehensive performance monitoring in your applications.
Setting Up Your Monitoring Infrastructure
Before implementing any specific metrics, you need to establish a robust monitoring infrastructure:
Data Collection Layer
interface PerformanceDataPoint { metric: string value: number timestamp: number userAgent: string connection: { effectiveType: string rtt: number downlink: number } device: { viewport: { width: number height: number } deviceMemory?: number hardwareConcurrency?: number } } class PerformanceMonitor { private buffer: PerformanceDataPoint[] = [] private flushInterval: number = 5000 constructor() { // Flush data periodically setInterval(() => this.flush(), this.flushInterval) // Listen for unload to send remaining data window.addEventListener('unload', () => { this.flush(true) }) } addDataPoint(metric: string, value: number) { const dataPoint: PerformanceDataPoint = { metric, value, timestamp: Date.now(), userAgent: navigator.userAgent, connection: this.getConnectionInfo(), device: this.getDeviceInfo() } this.buffer.push(dataPoint) } private getConnectionInfo() { const conn = (navigator as any).connection return { effectiveType: conn?.effectiveType || 'unknown', rtt: conn?.rtt || 0, downlink: conn?.downlink || 0 } } private getDeviceInfo() { return { viewport: { width: window.innerWidth, height: window.innerHeight }, deviceMemory: (navigator as any).deviceMemory, hardwareConcurrency: navigator.hardwareConcurrency } } private flush(isUnload = false) { if (this.buffer.length === 0) return const data = [...this.buffer] this.buffer = [] if (isUnload && navigator.sendBeacon) { navigator.sendBeacon('/analytics', JSON.stringify(data)) } else { fetch('/analytics', { method: 'POST', body: JSON.stringify(data), keepalive: true }) } } }
Real User Monitoring Implementation
Real User Monitoring (RUM) provides insights into actual user experiences. Here's how to implement comprehensive RUM:
class RUMCollector {
private performanceMonitor: PerformanceMonitor
constructor() {
this.performanceMonitor = new PerformanceMonitor()
this.setupObservers()
this.trackUserInteractions()
this.monitorNetworkRequests()
}
private setupObservers() {
// Performance Observer for various metrics
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
switch(entry.entryType) {
case 'largest-contentful-paint':
this.handleLCP(entry)
break
case 'layout-shift':
this.handleCLS(entry)
break
case 'first-input':
this.handleFID(entry)
break
case 'longtask':
this.handleLongTask(entry)
break
case 'resource':
this.handleResourceTiming(entry)
break
}
}
})
// Observe all relevant entry types
observer.observe({
entryTypes: [
'largest-contentful-paint',
'layout-shift',
'first-input',
'longtask',
'resource'
]
})
}
private trackUserInteractions() {
// Track clicks, scrolls, and other interactions
const interactionEvents = ['click', 'scroll', 'keypress']
interactionEvents.forEach(eventType => {
window.addEventListener(eventType, (e) => {
const interaction = {
type: eventType,
timestamp: performance.now(),
target: e.target instanceof Element ?
this.getElementPath(e.target) : 'unknown'
}
this.performanceMonitor.addDataPoint(
'user-interaction',
interaction
)
}, { passive: true })
})
}
private getElementPath(element: Element): string {
const path = []
let current = element
while (current && current !== document.body) {
let identifier = current.id ?
`#${current.id}` :
current.className ?
`.${current.className.split(' ').join('.')}` :
current.tagName.toLowerCase()
path.unshift(identifier)
current = current.parentElement as Element
}
return path.join(' > ')
}
private monitorNetworkRequests() {
// Monitor fetch requests
const originalFetch = window.fetch
window.fetch = async (...args) => {
const startTime = performance.now()
try {
const response = await originalFetch.apply(window, args)
this.recordNetworkTiming(
args[0].toString(),
startTime,
performance.now(),
response.status
)
return response
} catch (error) {
this.recordNetworkTiming(
args[0].toString(),
startTime,
performance.now(),
0,
error
)
throw error
}
}
// Monitor XMLHttpRequest
const originalXHR = window.XMLHttpRequest
window.XMLHttpRequest = function() {
const xhr = new originalXHR()
const startTime = performance.now()
xhr.addEventListener('load', () => {
this.recordNetworkTiming(
xhr.responseURL,
startTime,
performance.now(),
xhr.status
)
})
xhr.addEventListener('error', () => {
this.recordNetworkTiming(
xhr.responseURL,
startTime,
performance.now(),
0,
'XHR Error'
)
})
return xhr
}
}
private recordNetworkTiming(
url: string,
startTime: number,
endTime: number,
status: number,
error?: any
) {
const timing = {
url,
duration: endTime - startTime,
status,
error: error ? error.toString() : undefined
}
this.performanceMonitor.addDataPoint('network-request', timing)
}
}
Advanced Monitoring Strategies
- Error Tracking and Correlation
interface ErrorContext {
error: Error
componentStack?: string
performanceEntries: PerformanceEntry[]
userActions: UserAction[]
networkRequests: NetworkRequest[]
}
class ErrorTracker {
private static readonly PERFORMANCE_BUFFER_SIZE = 50
private performanceBuffer: PerformanceEntry[] = []
private userActionsBuffer: UserAction[] = []
private networkBuffer: NetworkRequest[] = []
constructor() {
this.setupPerformanceBuffering()
this.setupErrorHandling()
}
private setupPerformanceBuffering() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
this.performanceBuffer = [
...this.performanceBuffer,
...entries
].slice(-this.PERFORMANCE_BUFFER_SIZE)
})
observer.observe({ entryTypes: ['resource', 'navigation', 'longtask'] })
}
private setupErrorHandling() {
window.onerror = (msg, url, line, col, error) => {
this.handleError(error || new Error(msg as string))
}
window.addEventListener('unhandledrejection', (event) => {
this.handleError(event.reason)
})
}
private handleError(error: Error) {
const errorContext: ErrorContext = {
error,
performanceEntries: [...this.performanceBuffer],
userActions: [...this.userActionsBuffer],
networkRequests: [...this.networkBuffer]
}
// Analyze error context
this.analyzeErrorContext(errorContext)
// Send to error tracking service
this.reportError(errorContext)
}
private analyzeErrorContext(context: ErrorContext) {
// Look for performance issues around error
const timeWindow = 5000 // 5 seconds
const errorTime = Date.now()
const relevantPerformance = context.performanceEntries
.filter(entry => Math.abs(entry.startTime - errorTime) < timeWindow)
const relevantUserActions = context.userActions
.filter(action => Math.abs(action.timestamp - errorTime) < timeWindow)
// Identify potential causes
const possibleCauses = this.identifyPossibleCauses(
relevantPerformance,
relevantUserActions
)
context.analysis = {
possibleCauses,
performanceCorrelation: this.calculatePerformanceCorrelation(
relevantPerformance
)
}
}
}
Performance Testing and Validation
Performance testing is crucial for maintaining high standards and preventing regressions. Let's explore different approaches to performance testing and validation.
Unit Testing Performance Metrics
Unit tests for performance metrics help ensure that individual components meet performance budgets. Here's how to implement performance testing with Jest:
import { measureLCP, measureFID, measureCLS } from './performanceMetrics';
describe('Performance Metrics', () => {
test('LCP should be under 2.5s', async () => {
const lcp = await measureLCP();
expect(lcp).toBeLessThan(2500); // 2.5s in milliseconds
});
test('FID should be under 100ms', async () => {
const fid = await measureFID();
expect(fid).toBeLessThan(100);
});
test('CLS should be under 0.1', async () => {
const cls = await measureCLS();
expect(cls).toBeLessThan(0.1);
});
});
Integration Testing with Performance Budgets
Performance budgets help maintain performance standards across your application. Here's how to implement them using Lighthouse CI:
// lighthouserc.js
module.exports = {
ci: {
collect: {
numberOfRuns: 3,
url: ['http://localhost:3000/'],
},
assert: {
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['warn', { maxNumericValue: 300 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
End-to-End Performance Testing
E2E testing helps validate performance in real-world scenarios. Here's an example using Playwright:
import { test, expect } from '@playwright/test';
import { PerformanceObserver } from 'perf_hooks';
test('page load performance', async ({ page }) => {
// Start performance monitoring
const performanceMetrics = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
performanceMetrics.push(entry);
}
});
observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });
// Navigate to page
await page.goto('http://localhost:3000');
// Validate performance metrics
const navigationTiming = await page.evaluate(() => {
return JSON.stringify(performance.getEntriesByType('navigation')[0]);
});
const timing = JSON.parse(navigationTiming);
expect(timing.domContentLoadedEventEnd - timing.navigationStart).toBeLessThan(2000);
});
Synthetic Monitoring vs Real User Monitoring (RUM)
Both synthetic monitoring and RUM have their place in a comprehensive performance testing strategy:
Synthetic Monitoring
- Controlled environment testing
- Consistent baseline measurements
- Early detection of performance regressions
- Ideal for CI/CD pipelines
// Example synthetic monitoring setup
class SyntheticMonitor {
async measurePagePerformance(url: string) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const metrics = await page.metrics();
const performanceTimings = await page.evaluate(() => ({
FCP: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
LCP: performance.getEntriesByName('largest-contentful-paint')[0]?.startTime,
TTI: performance.getEntriesByName('time-to-interactive')[0]?.startTime
}));
await browser.close();
return { metrics, performanceTimings };
}
}
Real User Monitoring
- Actual user experience data
- Diverse device and network conditions
- Geographic performance variations
- Long-term performance trends
// Example RUM implementation
class RUMPerformanceCollector {
private metrics: PerformanceMetric[] = [];
constructor() {
this.initializeObservers();
}
private initializeObservers() {
// Core Web Vitals
new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => this.processMetric(entry));
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
// Navigation and Resource Timing
new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => this.processMetric(entry));
}).observe({ entryTypes: ['navigation', 'resource'] });
}
private processMetric(entry: PerformanceEntry) {
this.metrics.push({
name: entry.name,
type: entry.entryType,
startTime: entry.startTime,
duration: entry.duration,
timestamp: Date.now()
});
}
}
Combine both synthetic monitoring and RUM for the most comprehensive performance testing strategy. Synthetic monitoring provides consistent baselines, while RUM gives you real-world insights.
Optimization Strategies
1. Image Optimization
// Modern image loading
function loadOptimizedImage(src, width, height) {
const img = document.createElement('img');
// Use srcset for responsive images
img.srcset = `
${src}?w=400 400w,
${src}?w=800 800w,
${src}?w=1200 1200w
`;
// Use sizes to help browser choose right image
img.sizes = '(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px';
// Always set width/height to prevent layout shift
img.width = width;
img.height = height;
// Use loading="lazy" for images below the fold
img.loading = 'lazy';
return img;
}
// Next.js example with automatic optimization
import Image from 'next/image'
function OptimizedImage() {
return (
<Image
src="/hero.jpg"
width={1200}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
priority={true} // For above-the-fold images
/>
)
}
2. JavaScript Performance
// Code splitting example
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <LoadingSpinner />,
ssr: false // Skip server-side rendering if not needed
})
// Optimize third-party scripts
function loadThirdParty() {
const script = document.createElement('script')
script.src = 'https://third-party.com/widget.js'
script.async = true
script.defer = true
// Monitor performance impact
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const scriptEntry = entries[entries.length - 1]
if (scriptEntry.duration > 100) {
console.warn('Third-party script taking too long to execute')
}
})
observer.observe({ entryTypes: ['longtask'] })
document.body.appendChild(script)
}
3. CSS Performance
/* Critical CSS inline in head */
.hero {
content-visibility: auto;
contain-intrinsic-size: 0 500px;
}
/* Use @media for conditional loading */
@media (min-width: 768px) {
@import url('desktop.css');
}
/* Optimize animations */
.animate {
transform: translateZ(0); /* Hardware acceleration */
will-change: transform; /* Hint to browser */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
Monitoring Tools Integration
1. Custom Performance Dashboard
interface PerformanceMetrics {
lcp: number
fid: number
cls: number
ttfb: number
fcp: number
}
class PerformanceDashboard {
private metrics: PerformanceMetrics[] = []
addMetric(metric: keyof PerformanceMetrics, value: number) {
const timestamp = Date.now()
this.metrics.push({
...this.getEmptyMetrics(),
[metric]: value,
timestamp
})
this.analyze()
}
analyze() {
const recentMetrics = this.metrics.slice(-100)
const averages = this.calculateAverages(recentMetrics)
if (averages.lcp > 2500 || averages.cls > 0.1) {
this.alertPerformanceIssue(averages)
}
}
private calculateAverages(metrics: PerformanceMetrics[]): PerformanceMetrics {
// Implementation
}
}
2. Automated Performance Testing
# .github/workflows/performance.yml
name: Performance Testing
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://your-site.com/
https://your-site.com/products
budgetPath: ./budget.json
uploadArtifacts: true
- name: Check performance budget
run: |
if [ ${{ steps.lighthouse.outputs.scores.performance }} -lt 90 ]; then
echo "Performance score below threshold"
exit 1
fi
Case Studies
E-commerce Site Optimization
A major e-commerce site improved their Core Web Vitals:
- LCP: 3.2s → 1.8s
- CLS: 0.15 → 0.05
- FID: 150ms → 80ms
Result: 23% increase in conversion rate
Key strategies:
- Image optimization and CDN implementation
- Critical CSS extraction
- Route-based code splitting
- Server-side rendering for product pages
News Website Performance
// Implementing infinite scroll with performance in mind
function InfiniteNewsFeed() {
const [articles, setArticles] = useState([])
const [page, setPage] = useState(1)
// Use Intersection Observer for efficient loading
const observer = useRef(
new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMoreArticles()
}
},
{ threshold: 0.5 }
)
)
async function loadMoreArticles() {
const newArticles = await fetchArticles(page)
// Optimize DOM updates
requestAnimationFrame(() => {
setArticles(prev => [...prev, ...newArticles])
setPage(prev => prev + 1)
})
}
return (
<div>
{articles.map(article => (
<Article
key={article.id}
data={article}
loading="lazy"
/>
))}
<div ref={loadingRef} />
</div>
)
}
Advanced Topics
1. Memory Leaks Detection
// Monitor memory usage
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const jsHeapSize = entries[0].jsHeapSizeLimit;
const usedJSHeap = entries[0].usedJSHeapSize;
if (usedJSHeap / jsHeapSize > 0.9) {
console.warn('High memory usage detected');
// Implement cleanup strategies
}
});
observer.observe({ entryTypes: ['memory'] });
// Detect DOM leaks
function detectDOMLeaks() {
const initialNodes = document.getElementsByTagName('*').length;
return setInterval(() => {
const currentNodes = document.getElementsByTagName('*').length;
const diff = currentNodes - initialNodes;
if (diff > 1000) {
console.warn(`Possible DOM leak: ${diff} new nodes`);
}
}, 10000);
}
2. Worker Performance
// Offload heavy computations to Web Worker
const worker = new Worker('worker.js');
interface WorkerTask {
id: string;
data: any;
type: 'HEAVY_COMPUTATION' | 'DATA_PROCESSING';
}
// Main thread
function sendTaskToWorker(task: WorkerTask) {
const startTime = performance.now();
worker.postMessage(task);
worker.onmessage = (e) => {
const duration = performance.now() - startTime;
if (duration > 100) {
console.warn(`Task ${task.id} took ${duration}ms`);
}
processWorkerResult(e.data);
};
}
// worker.js
self.onmessage = async (e) => {
const task = e.data;
// Use transferable objects for better performance
const result = await processTask(task);
self.postMessage(result, [result.buffer]);
};
Tools and Resources
- ScreenScout Performance Monitoring
- Google Lighthouse
- WebPageTest
- Chrome DevTools Performance Panel
- web-vitals library
Ready to optimize your website's performance? Try ScreenScout's Performance Monitoring for free and get detailed insights into your site's performance metrics!