# Chapter 10: Performance Optimization *"The best optimization is the request you don't make."* --- ## The Scale Crisis Weather Buddy had grown beyond Sarah's wildest dreams. Millions of users, thousands of cities, real-time updates flowing constantly. But success brought problems. "Sarah, we need to talk," the CTO said, showing her a graph. "Our API costs are through the roof. We're making 50 million requests per day, and half of them are duplicates." Sarah looked at the metrics. Multiple users requesting the same city weather. The same user checking repeatedly. Connections hanging open. Memory usage climbing. "Time for the advanced course," Marcus said. "Let's optimize TypedFetch to handle planet-scale traffic." ## Request Deduplication: Never Ask Twice The most powerful optimization is avoiding duplicate work: ```typescript // The Problem: Multiple components request same data // Component A const weather1 = await tf.get('/api/weather/london') // Component B (100ms later) const weather2 = await tf.get('/api/weather/london') // Duplicate! // Component C (200ms later) const weather3 = await tf.get('/api/weather/london') // Another duplicate! // The Solution: TypedFetch deduplicates automatically const weather1 = await tf.get('/api/weather/london') // Network request const weather2 = await tf.get('/api/weather/london') // Returns same promise! const weather3 = await tf.get('/api/weather/london') // Still same promise! // All three get the same response from ONE network request ``` ## Advanced Deduplication Strategies TypedFetch's deduplication is configurable and intelligent: ```typescript // Configure deduplication tf.configure({ deduplication: { enabled: true, window: 100, // Dedupe requests within 100ms // Custom key generation keyGenerator: (config) => { // Include user ID for user-specific endpoints if (config.url.includes('/user/')) { return `${config.url}:${getCurrentUserId()}` } // Include critical headers if (config.headers['Accept-Language']) { return `${config.url}:${config.headers['Accept-Language']}` } return config.url }, // Exclude certain requests exclude: [ '/api/analytics/*', // Don't dedupe analytics '/api/auth/*' // Don't dedupe auth ] } }) // Manual deduplication control const { data } = await tf.get('/api/weather', { dedupe: false // Force new request }) // Share requests across components class RequestCoordinator { private pending = new Map>() async get(url: string): Promise { // Check if request is already in-flight if (this.pending.has(url)) { return this.pending.get(url)! } // Create new request const promise = tf.get(url) .finally(() => { // Clean up after completion this.pending.delete(url) }) this.pending.set(url, promise) return promise } } ``` ## Connection Pooling: Reuse, Don't Recreate HTTP connections are expensive. Reuse them: ```typescript // Configure connection pooling tf.configure({ connections: { // HTTP/1.1 settings maxSockets: 10, // Max connections per host maxFreeSockets: 5, // Keep idle connections timeout: 60000, // Socket timeout keepAlive: true, // Enable keep-alive keepAliveMsecs: 1000, // Keep-alive interval // HTTP/2 settings enableHTTP2: true, // Use HTTP/2 when available sessionTimeout: 60000, // HTTP/2 session timeout // Connection strategies strategy: 'aggressive', // 'aggressive' | 'balanced' | 'conservative' // Per-host configuration hosts: { 'api.weather.com': { maxSockets: 20, // More connections for critical API enableHTTP2: true }, 'cdn.example.com': { maxSockets: 50, // Many connections for CDN keepAlive: false // Don't keep CDN connections } } } }) // Monitor connection pool const poolStats = tf.getConnectionStats() console.log(poolStats) // { // 'api.weather.com': { // active: 8, // idle: 2, // pending: 0, // protocol: 'h2', // reused: 145, // created: 10 // } // } ``` ## Memory Management: Don't Leak, Don't Bloat Track and limit memory usage: ```typescript // Memory-aware configuration tf.configure({ memory: { maxCacheSize: 100 * 1024 * 1024, // 100MB cache limit maxResponseSize: 10 * 1024 * 1024, // 10MB max response // Response compression compression: { enabled: true, algorithms: ['gzip', 'br', 'deflate'] }, // Automatic garbage collection gc: { interval: 60000, // Run every minute idleOnly: true, // Only when idle aggressive: false // Gentle cleaning }, // Memory pressure handling onMemoryPressure: (usage) => { if (usage.percent > 80) { tf.cache.evict(0.5) // Evict 50% of cache } } } }) // Object pooling for frequent allocations class ResponsePool { private pool: Response[] = [] private maxSize = 100 acquire(): Response { return this.pool.pop() || new Response() } release(response: Response) { if (this.pool.length < this.maxSize) { response.reset() // Clear data this.pool.push(response) } } } // Monitor memory usage const memStats = tf.getMemoryStats() console.log(memStats) // { // cache: { size: 45000000, items: 1523 }, // connections: { active: 10, pooled: 5 }, // pending: { requests: 3, size: 15000 }, // total: 45015000 // } ``` ## Weather Buddy 10.0: Planet Scale Let's optimize Weather Buddy for millions of users: ```typescript // weather-buddy-10.ts import { tf, createTypedFetch } from 'typedfetch' // Performance monitoring class PerformanceMonitor { private metrics = { requests: 0, cacheHits: 0, dedupedRequests: 0, bytesTransferred: 0, connectionReuse: 0, avgLatency: 0, p95Latency: 0, p99Latency: 0 } private latencies: number[] = [] private startTime = Date.now() recordRequest(stats: RequestStats) { this.metrics.requests++ if (stats.cached) { this.metrics.cacheHits++ } if (stats.deduped) { this.metrics.dedupedRequests++ } if (stats.connectionReused) { this.metrics.connectionReuse++ } this.metrics.bytesTransferred += stats.bytes this.latencies.push(stats.duration) // Keep last 1000 latencies if (this.latencies.length > 1000) { this.latencies.shift() } this.updateLatencyMetrics() } private updateLatencyMetrics() { const sorted = [...this.latencies].sort((a, b) => a - b) this.metrics.avgLatency = sorted.reduce((a, b) => a + b, 0) / sorted.length this.metrics.p95Latency = sorted[Math.floor(sorted.length * 0.95)] this.metrics.p99Latency = sorted[Math.floor(sorted.length * 0.99)] } getReport() { const runtime = (Date.now() - this.startTime) / 1000 const rps = this.metrics.requests / runtime return { ...this.metrics, runtime: `${runtime.toFixed(1)}s`, requestsPerSecond: rps.toFixed(2), cacheHitRate: ((this.metrics.cacheHits / this.metrics.requests) * 100).toFixed(1) + '%', dedupRate: ((this.metrics.dedupedRequests / this.metrics.requests) * 100).toFixed(1) + '%', connectionReuseRate: ((this.metrics.connectionReuse / this.metrics.requests) * 100).toFixed(1) + '%', bandwidth: this.formatBytes(this.metrics.bytesTransferred / runtime) + '/s' } } private formatBytes(bytes: number): string { if (bytes < 1024) return bytes + ' B' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } } // Optimized Weather Service class OptimizedWeatherService { private tf: TypedFetch private monitor = new PerformanceMonitor() private popularCities = new Set() private userPatterns = new Map() constructor() { // Create optimized instance this.tf = createTypedFetch({ // Aggressive caching cache: { algorithm: 'W-TinyLFU', maxSize: 200 * 1024 * 1024, // 200MB maxAge: 300000, // 5 minutes default staleWhileRevalidate: true }, // Smart deduplication deduplication: { enabled: true, window: 500, keyGenerator: this.generateCacheKey.bind(this) }, // Connection optimization connections: { enableHTTP2: true, maxSockets: 20, keepAlive: true, strategy: 'aggressive' }, // Memory management memory: { maxResponseSize: 5 * 1024 * 1024, compression: { enabled: true }, gc: { interval: 30000 } } }) this.setupInterceptors() this.startOptimizations() } private setupInterceptors() { // Performance tracking this.tf.addRequestInterceptor(config => { config.metadata.startTime = performance.now() return config }) this.tf.addResponseInterceptor(response => { const duration = performance.now() - response.config.metadata.startTime this.monitor.recordRequest({ duration, cached: response.cached || false, deduped: response.config.metadata.deduped || false, connectionReused: response.config.metadata.connectionReused || false, bytes: JSON.stringify(response.data).length }) // Track popular cities const city = this.extractCity(response.config.url) if (city) { this.trackCityPopularity(city) } return response }) // Compression this.tf.addRequestInterceptor(config => { config.headers['Accept-Encoding'] = 'gzip, deflate, br' return config }) } private generateCacheKey(config: RequestConfig): string { const url = new URL(config.url) const city = url.pathname.split('/').pop() // User-specific data if (url.pathname.includes('/user/')) { return `${config.url}:${this.getCurrentUserId()}` } // Language-specific weather descriptions const lang = config.headers['Accept-Language'] || 'en' return `${config.url}:${lang}` } private startOptimizations() { // Preload popular cities setInterval(() => { this.preloadPopularCities() }, 60000) // Every minute // Predictive prefetching this.setupPredictivePrefetch() // Connection warming this.warmConnections() } private async preloadPopularCities() { const topCities = Array.from(this.popularCities) .sort((a, b) => (this.getCityScore(b) || 0) - (this.getCityScore(a) || 0) ) .slice(0, 20) console.log('Preloading popular cities:', topCities) // Batch preload await Promise.all( topCities.map(city => this.tf.get(`/api/weather/${city}`, { priority: 'low', cache: { warm: true } }).catch(() => {}) // Ignore errors ) ) } private setupPredictivePrefetch() { // Track user patterns this.tf.addResponseInterceptor(response => { const userId = this.getCurrentUserId() const city = this.extractCity(response.config.url) if (userId && city) { this.updateUserPattern(userId, city) } return response }) } private updateUserPattern(userId: string, city: string) { if (!this.userPatterns.has(userId)) { this.userPatterns.set(userId, { cities: new Map(), lastAccess: new Map() }) } const pattern = this.userPatterns.get(userId)! const now = Date.now() const hour = new Date().getHours() // Track access patterns if (!pattern.cities.has(city)) { pattern.cities.set(city, { count: 0, hours: new Array(24).fill(0) }) } const cityPattern = pattern.cities.get(city)! cityPattern.count++ cityPattern.hours[hour]++ pattern.lastAccess.set(city, now) // Predictive prefetch this.schedulePrefetch(userId, pattern) } private schedulePrefetch(userId: string, pattern: UserPattern) { const now = new Date() const nextHour = new Date(now) nextHour.setHours(now.getHours() + 1, 0, 0, 0) const delay = nextHour.getTime() - now.getTime() setTimeout(() => { this.prefetchForUser(userId, pattern, nextHour.getHours()) }, delay) } private async prefetchForUser(userId: string, pattern: UserPattern, hour: number) { // Find cities user typically checks at this hour const citiesToPrefetch = Array.from(pattern.cities.entries()) .filter(([_, cityPattern]) => cityPattern.hours[hour] > 2) .map(([city]) => city) if (citiesToPrefetch.length > 0) { console.log(`Prefetching for user ${userId} at hour ${hour}:`, citiesToPrefetch) await Promise.all( citiesToPrefetch.map(city => this.getWeather(city, { prefetch: true }) ) ) } } private warmConnections() { // Keep connections alive to critical endpoints const endpoints = [ 'api.weather.com', 'api.alerts.com', 'cdn.weather.com' ] endpoints.forEach(host => { // HTTP/2 PING frames setInterval(() => { this.tf.ping(`https://${host}`) }, 30000) }) } // Optimized weather fetching async getWeather(city: string, options: WeatherOptions = {}) { // Check if this is a popular city const isPopular = this.popularCities.has(city) const { data } = await this.tf.get(`/api/weather/${city}`, { priority: options.prefetch ? 'low' : 'normal', cache: { maxAge: isPopular ? 600000 : 300000, // 10min for popular, 5min for others staleWhileRevalidate: true }, // Timeout based on priority timeout: options.priority === 'high' ? 5000 : 10000 }) return data } async getWeatherBatch(cities: string[]) { // Deduplicate const uniqueCities = [...new Set(cities)] // Split into chunks for parallel fetching const chunks = [] const chunkSize = 10 for (let i = 0; i < uniqueCities.length; i += chunkSize) { chunks.push(uniqueCities.slice(i, i + chunkSize)) } // Fetch chunks in parallel const results = await Promise.all( chunks.map(chunk => Promise.all( chunk.map(city => this.getWeather(city) .then(data => ({ city, data, error: null })) .catch(error => ({ city, data: null, error })) ) ) ) ) // Flatten results return results.flat() } getPerformanceReport() { return this.monitor.getReport() } private trackCityPopularity(city: string) { this.popularCities.add(city) // Additional scoring logic could go here } private getCityScore(city: string): number { // Simple scoring based on access frequency // In production, would use more sophisticated algorithm return 1 } private extractCity(url: string): string | null { const match = url.match(/\/weather\/([^/?]+)/) return match ? match[1] : null } private getCurrentUserId(): string | null { return localStorage.getItem('userId') } } // Request batching for efficiency class RequestBatcher { private queue = new Map() private batchDelay = 50 // 50ms batching window private maxBatchSize = 20 constructor(private service: OptimizedWeatherService) { this.processBatches() } async get(city: string): Promise { return new Promise((resolve, reject) => { if (!this.queue.has(city)) { this.queue.set(city, []) } this.queue.get(city)!.push({ resolve, reject, timestamp: Date.now() }) }) } private async processBatches() { setInterval(async () => { if (this.queue.size === 0) return // Get all pending requests const cities = Array.from(this.queue.keys()) const batch = cities.slice(0, this.maxBatchSize) // Clear from queue const handlers = new Map() batch.forEach(city => { handlers.set(city, this.queue.get(city)!) this.queue.delete(city) }) try { // Batch fetch const results = await this.service.getWeatherBatch(batch) // Resolve individual promises results.forEach(({ city, data, error }) => { const cityHandlers = handlers.get(city) || [] cityHandlers.forEach(handler => { if (error) { handler.reject(error) } else { handler.resolve(data) } }) }) } catch (error) { // Reject all handlers handlers.forEach(cityHandlers => { cityHandlers.forEach(handler => handler.reject(error)) }) } }, this.batchDelay) } } // Performance dashboard class PerformanceDashboard { private service: OptimizedWeatherService private chart?: Chart constructor(service: OptimizedWeatherService) { this.service = service this.init() } private init() { this.createUI() this.startMonitoring() } private createUI() { const dashboard = document.createElement('div') dashboard.className = 'performance-dashboard' dashboard.innerHTML = `

Performance Metrics

0
0%
0%
0ms
0ms
0 KB/s
` document.body.appendChild(dashboard) } private startMonitoring() { setInterval(() => { const report = this.service.getPerformanceReport() // Update metrics document.getElementById('rps')!.textContent = report.requestsPerSecond document.getElementById('cache-hit')!.textContent = report.cacheHitRate document.getElementById('dedup')!.textContent = report.dedupRate document.getElementById('latency')!.textContent = Math.round(report.avgLatency) + 'ms' document.getElementById('p95')!.textContent = Math.round(report.p95Latency) + 'ms' document.getElementById('bandwidth')!.textContent = report.bandwidth // Update chart this.updateChart(report) }, 1000) } private updateChart(report: any) { // Chart implementation } } // Initialize optimized service const weatherService = new OptimizedWeatherService() const batcher = new RequestBatcher(weatherService) const dashboard = new PerformanceDashboard(weatherService) // Export for global access (window as any).weatherService = { getWeather: (city: string) => batcher.get(city), getPerformance: () => weatherService.getPerformanceReport() } ``` ## Advanced Optimization Techniques ### 1. Smart Request Prioritization Not all requests are equal: ```typescript class PriorityQueue { private queues = { high: [] as QueueItem[], normal: [] as QueueItem[], low: [] as QueueItem[] } enqueue(item: T, priority: Priority = 'normal') { this.queues[priority].push({ item, timestamp: Date.now() }) } dequeue(): T | undefined { // High priority first if (this.queues.high.length > 0) { return this.queues.high.shift()!.item } // Normal priority if (this.queues.normal.length > 0) { return this.queues.normal.shift()!.item } // Low priority only if idle if (this.queues.low.length > 0 && this.isIdle()) { return this.queues.low.shift()!.item } return undefined } private isIdle(): boolean { return this.queues.high.length === 0 && this.queues.normal.length === 0 } } // Priority-aware request scheduler tf.configure({ scheduler: { enabled: true, maxConcurrent: 6, priorityLevels: ['high', 'normal', 'low'], // Starvation prevention maxWaitTime: { high: 1000, // 1 second max wait normal: 5000, // 5 seconds max wait low: 30000 // 30 seconds max wait } } }) ``` ### 2. Response Streaming Optimization Stream large responses efficiently: ```typescript class StreamOptimizer { async streamLargeResponse(url: string) { const response = await fetch(url) if (!response.body) { throw new Error('No response body') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' const results = [] while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // Process complete JSON objects const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.trim()) { try { const obj = JSON.parse(line) results.push(obj) // Process in chunks if (results.length >= 100) { await this.processBatch(results.splice(0, 100)) } } catch (e) { console.error('Parse error:', e) } } } } // Process remaining if (results.length > 0) { await this.processBatch(results) } } private async processBatch(items: any[]) { // Process items without blocking UI await new Promise(resolve => setTimeout(resolve, 0)) items.forEach(item => { // Process each item }) } } ``` ### 3. Bundle Size Optimization Keep TypedFetch lean: ```typescript // Tree-shakeable imports import { get, post } from 'typedfetch/core' import { cache } from 'typedfetch/cache' import { retry } from 'typedfetch/retry' // Only import what you need const tf = createTypedFetch({ modules: [cache, retry] // Only these features }) // Dynamic imports for optional features async function enableDebugMode() { const { debug } = await import('typedfetch/debug') tf.use(debug) } // Code splitting by route const routes = { '/dashboard': () => import('./dashboard'), '/analytics': () => import('./analytics'), '/settings': () => import('./settings') } ``` ### 4. Worker Thread Offloading Move heavy processing off the main thread: ```typescript // worker.ts self.addEventListener('message', async (event) => { const { type, data } = event.data switch (type) { case 'parse-large-json': const parsed = JSON.parse(data) self.postMessage({ type: 'parsed', data: parsed }) break case 'compress-data': const compressed = await compress(data) self.postMessage({ type: 'compressed', data: compressed }) break } }) // main.ts class WorkerPool { private workers: Worker[] = [] private queue: Task[] = [] private busy = new Set() constructor(size = 4) { for (let i = 0; i < size; i++) { this.workers.push(new Worker('worker.js')) } } async process(type: string, data: any): Promise { const worker = await this.getWorker() return new Promise((resolve, reject) => { worker.onmessage = (event) => { this.release(worker) resolve(event.data) } worker.onerror = (error) => { this.release(worker) reject(error) } worker.postMessage({ type, data }) }) } private async getWorker(): Promise { // Find available worker const available = this.workers.find(w => !this.busy.has(w)) if (available) { this.busy.add(available) return available } // Wait for one to be free return new Promise(resolve => { const check = setInterval(() => { const free = this.workers.find(w => !this.busy.has(w)) if (free) { clearInterval(check) this.busy.add(free) resolve(free) } }, 10) }) } private release(worker: Worker) { this.busy.delete(worker) } } ``` ## Performance Monitoring Track everything to optimize effectively: ```typescript // Comprehensive performance tracking class PerformanceTracker { private marks = new Map() private measures = new Map() mark(name: string) { this.marks.set(name, performance.now()) } measure(name: string, startMark: string, endMark?: string) { const start = this.marks.get(startMark) const end = endMark ? this.marks.get(endMark) : performance.now() if (!start) return const duration = end! - start if (!this.measures.has(name)) { this.measures.set(name, []) } this.measures.get(name)!.push(duration) // Send to analytics if (this.shouldReport()) { this.report() } } getStats(name: string) { const measures = this.measures.get(name) || [] if (measures.length === 0) { return null } const sorted = [...measures].sort((a, b) => a - b) return { count: measures.length, min: sorted[0], max: sorted[sorted.length - 1], avg: measures.reduce((a, b) => a + b, 0) / measures.length, median: sorted[Math.floor(sorted.length / 2)], p95: sorted[Math.floor(sorted.length * 0.95)], p99: sorted[Math.floor(sorted.length * 0.99)] } } private shouldReport(): boolean { // Report every 100 measures or 60 seconds const totalMeasures = Array.from(this.measures.values()) .reduce((sum, arr) => sum + arr.length, 0) return totalMeasures >= 100 } private report() { const report = { timestamp: Date.now(), metrics: {} as any } this.measures.forEach((values, name) => { report.metrics[name] = this.getStats(name) }) // Send to monitoring service navigator.sendBeacon('/api/metrics', JSON.stringify(report)) // Clear old data this.measures.clear() } } // Use throughout the app const perf = new PerformanceTracker() perf.mark('request-start') const data = await tf.get('/api/data') perf.measure('api-request', 'request-start') ``` ## Best Practices for Performance 🎯 ### 1. Measure First, Optimize Second ```typescript // Profile before optimizing const profile = await tf.profile(async () => { // Your code here }) console.log(profile) // { // duration: 234, // memory: { before: 1024000, after: 2048000 }, // network: { requests: 5, bytes: 150000 } // } ``` ### 2. Set Performance Budgets ```typescript tf.configure({ performance: { budgets: { requestDuration: 1000, // 1 second max bundleSize: 100000, // 100KB max memoryUsage: 50000000, // 50MB max onBudgetExceeded: (metric, value, budget) => { console.error(`Performance budget exceeded: ${metric} = ${value} (budget: ${budget})`) // Report to monitoring reportPerformanceIssue(metric, value, budget) } } } }) ``` ### 3. Progressive Enhancement ```typescript // Start with basics, add features as needed const tf = createMinimalTypedFetch() // Add features based on device capabilities if (navigator.connection?.effectiveType === '4g') { tf.use(aggressiveCache) tf.use(prefetching) } if (navigator.deviceMemory > 4) { tf.use(largeCache) } if ('serviceWorker' in navigator) { tf.use(offlineSupport) } ``` ### 4. Lazy Load Heavy Features ```typescript // Only load what's needed async function enableAdvancedFeatures() { const [ { streaming }, { websocket }, { analytics } ] = await Promise.all([ import('typedfetch/streaming'), import('typedfetch/websocket'), import('typedfetch/analytics') ]) tf.use(streaming) tf.use(websocket) tf.use(analytics) } ``` ## Practice Time! 🏋️ ### Exercise 1: Build a Request Deduplicator Create a deduplication system: ```typescript class Deduplicator { // Your code here: // - Track in-flight requests // - Return existing promises // - Handle errors properly // - Clean up completed requests } ``` ### Exercise 2: Implement Connection Pooling Build a connection pool: ```typescript class ConnectionPool { // Your code here: // - Manage connection lifecycle // - Reuse idle connections // - Handle connection limits // - Monitor pool health } ``` ### Exercise 3: Create a Performance Monitor Build comprehensive monitoring: ```typescript class PerformanceMonitor { // Your code here: // - Track all metrics // - Calculate percentiles // - Detect anomalies // - Generate reports } ``` ## Key Takeaways 🎯 1. **Deduplication prevents duplicate requests** - Same data, one request 2. **Connection pooling reduces overhead** - Reuse, don't recreate 3. **Smart caching is the biggest win** - W-TinyLFU beats LRU 4. **Memory management prevents leaks** - Monitor and limit usage 5. **Prioritization improves perceived performance** - Important stuff first 6. **Batching reduces overhead** - Combine multiple requests 7. **Monitoring enables optimization** - Can't improve what you don't measure 8. **Progressive enhancement scales** - Start simple, add as needed ## Common Pitfalls 🚨 1. **Optimizing without measuring** - Profile first 2. **Over-caching dynamic data** - Some things need to be fresh 3. **Ignoring memory limits** - Mobile devices have less RAM 4. **Too aggressive deduplication** - User-specific data differs 5. **Not monitoring production** - Dev !== Production 6. **Premature optimization** - Focus on bottlenecks ## What's Next? You've mastered performance optimization! But what happens when users go offline? In Chapter 11, we'll explore offline support and progressive enhancement: - Service Worker integration - Offline request queuing - Background sync - IndexedDB caching - Conflict resolution - Progressive Web App features Ready to make your app work anywhere? See you in Chapter 11! 📱 --- ## Chapter Summary - Request deduplication eliminates redundant network calls automatically - Connection pooling with HTTP/2 reduces connection overhead significantly - Memory management with object pooling and garbage collection prevents leaks - Smart caching strategies based on popularity and user patterns improve hit rates - Performance monitoring with detailed metrics enables data-driven optimization - Batching and prioritization improve perceived performance for users - Weather Buddy 10.0 handles millions of users with intelligent optimizations - Always measure before optimizing and set performance budgets **Next Chapter Preview**: Offline & Progressive Enhancement - Make your app work without internet using Service Workers, background sync, and conflict resolution.