Features: - Zero configuration, just works out of the box - Runtime type inference and validation - Built-in caching with W-TinyLFU algorithm - Automatic retries with exponential backoff - Circuit breaker for resilience - Request deduplication - Offline support with queue - OpenAPI schema discovery - Full TypeScript support with type descriptors - Modular architecture - Configurable for advanced use cases Built with bun, ready for npm publishing
31 KiB
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:
// 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:
// 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<string, Promise<any>>()
async get<T>(url: string): Promise<T> {
// Check if request is already in-flight
if (this.pending.has(url)) {
return this.pending.get(url)!
}
// Create new request
const promise = tf.get<T>(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:
// 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:
// 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:
// 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<string>()
private userPatterns = new Map<string, UserPattern>()
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<string, {
resolve: Function
reject: Function
timestamp: number
}[]>()
private batchDelay = 50 // 50ms batching window
private maxBatchSize = 20
constructor(private service: OptimizedWeatherService) {
this.processBatches()
}
async get(city: string): Promise<WeatherData> {
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<string, typeof this.queue.get('')>()
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 = `
<h3>Performance Metrics</h3>
<div class="metrics-grid">
<div class="metric">
<label>Requests/sec</label>
<span id="rps">0</span>
</div>
<div class="metric">
<label>Cache Hit Rate</label>
<span id="cache-hit">0%</span>
</div>
<div class="metric">
<label>Dedup Rate</label>
<span id="dedup">0%</span>
</div>
<div class="metric">
<label>Avg Latency</label>
<span id="latency">0ms</span>
</div>
<div class="metric">
<label>P95 Latency</label>
<span id="p95">0ms</span>
</div>
<div class="metric">
<label>Bandwidth</label>
<span id="bandwidth">0 KB/s</span>
</div>
</div>
<canvas id="perf-chart" width="400" height="200"></canvas>
`
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:
class PriorityQueue<T> {
private queues = {
high: [] as QueueItem<T>[],
normal: [] as QueueItem<T>[],
low: [] as QueueItem<T>[]
}
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:
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:
// 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:
// 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<Worker>()
constructor(size = 4) {
for (let i = 0; i < size; i++) {
this.workers.push(new Worker('worker.js'))
}
}
async process(type: string, data: any): Promise<any> {
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<Worker> {
// 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:
// Comprehensive performance tracking
class PerformanceTracker {
private marks = new Map<string, number>()
private measures = new Map<string, number[]>()
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
// 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
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
// 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
// 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:
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:
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:
class PerformanceMonitor {
// Your code here:
// - Track all metrics
// - Calculate percentiles
// - Detect anomalies
// - Generate reports
}
Key Takeaways 🎯
- Deduplication prevents duplicate requests - Same data, one request
- Connection pooling reduces overhead - Reuse, don't recreate
- Smart caching is the biggest win - W-TinyLFU beats LRU
- Memory management prevents leaks - Monitor and limit usage
- Prioritization improves perceived performance - Important stuff first
- Batching reduces overhead - Combine multiple requests
- Monitoring enables optimization - Can't improve what you don't measure
- Progressive enhancement scales - Start simple, add as needed
Common Pitfalls 🚨
- Optimizing without measuring - Profile first
- Over-caching dynamic data - Some things need to be fresh
- Ignoring memory limits - Mobile devices have less RAM
- Too aggressive deduplication - User-specific data differs
- Not monitoring production - Dev !== Production
- 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.