TypeFetched/manual/chapter-10-performance.md
Casey Collier b85b9a63e2 Initial commit: TypedFetch - Zero-dependency, type-safe HTTP client
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
2025-07-20 12:35:43 -04:00

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 🎯

  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.