Back to Blog

Latency Budgeting in Frontend Systems

Performance as a System Design Discipline

Latency budgeting transforms performance from a reactive concern ("this page is slow, fix it") into a proactive architectural constraint. By allocating specific time budgets to each phase of request processing—DNS, TLS, CDN, API, database, rendering, hydration—teams can make informed trade-offs, identify bottlenecks before they become problems, and design systems that meet performance targets by construction.

This article presents a comprehensive framework for latency budgeting that treats performance as a first-class architectural concern.


The Anatomy of Frontend Latency

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Request Lifecycle Latency Breakdown                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  User Click                                                                 │
│      │                                                                      │
│      ▼                                                                      │
│  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌──────────┐ │
│  │  DNS   │─▶│  TCP   │─▶│  TLS   │─▶│ Request│─▶│Response│─▶│ Download │ │
│  │        │  │Connect │  │Handshk │  │  Sent  │  │ Start  │  │          │ │
│  └────────┘  └────────┘  └────────┘  └────────┘  └────────┘  └──────────┘ │
│    20ms        30ms        50ms        10ms        200ms        100ms      │
│                                                                             │
│                            NETWORK (410ms)                                  │
│                                                                             │
│  ┌────────────────────────────────────────────────────────────────────────┐│
│  │                                                                         ││
│  │  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────────────┐   ││
│  │  │ Parse  │─▶│ Style  │─▶│ Layout │─▶│ Paint  │─▶│   Hydration    │   ││
│  │  │  HTML  │  │ Calc   │  │        │  │        │  │                │   ││
│  │  └────────┘  └────────┘  └────────┘  └────────┘  └────────────────┘   ││
│  │    50ms        30ms        20ms        30ms           200ms            ││
│  │                                                                         ││
│  │                       BROWSER (330ms)                                   ││
│  │                                                                         ││
│  └────────────────────────────────────────────────────────────────────────┘│
│                                                                             │
│  ┌────────────────────────────────────────────────────────────────────────┐│
│  │  JavaScript Execution                                                   ││
│  │  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────────────┐   ││
│  │  │ Parse  │─▶│Compile │─▶│Execute │─▶│ React  │─▶│   Interactive  │   ││
│  │  │   JS   │  │   JS   │  │  Init  │  │ Render │  │                │   ││
│  │  └────────┘  └────────┘  └────────┘  └────────┘  └────────────────┘   ││
│  │    100ms       50ms        150ms       100ms                           ││
│  │                                                                         ││
│  │                    JAVASCRIPT (400ms)                                   ││
│  │                                                                         ││
│  └────────────────────────────────────────────────────────────────────────┘│
│                                                                             │
│  Total Time to Interactive: ~1140ms                                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Budget Allocation Framework

Defining the Total Budget

// src/performance/budget.ts

interface LatencyBudget {
  total: number;           // Total time to interactive (ms)
  phases: PhaseBudgets;
  margins: {
    safety: number;        // Buffer for variance (%)
    degradation: number;   // Acceptable degradation threshold (%)
  };
}

interface PhaseBudgets {
  // Network phases
  dns: number;
  tcp: number;
  tls: number;
  ttfb: number;           // Time to first byte (server processing)
  download: number;

  // Server phases (included in TTFB)
  edgeCompute: number;
  originRouting: number;
  apiGateway: number;
  database: number;
  serialization: number;

  // Browser phases
  parsing: number;
  styleCalculation: number;
  layout: number;
  paint: number;
  compositing: number;

  // JavaScript phases
  jsParsing: number;
  jsCompilation: number;
  jsExecution: number;
  hydration: number;
  reactRender: number;
}

// Example budget for a P75 target of 2 seconds on 4G
const productionBudget: LatencyBudget = {
  total: 2000,
  phases: {
    // Network: 600ms (30%)
    dns: 20,
    tcp: 30,
    tls: 50,
    ttfb: 400,
    download: 100,

    // Server (within TTFB): 400ms
    edgeCompute: 50,
    originRouting: 30,
    apiGateway: 50,
    database: 200,
    serialization: 70,

    // Browser: 400ms (20%)
    parsing: 100,
    styleCalculation: 80,
    layout: 80,
    paint: 100,
    compositing: 40,

    // JavaScript: 800ms (40%)
    jsParsing: 150,
    jsCompilation: 100,
    jsExecution: 200,
    hydration: 200,
    reactRender: 150,
  },
  margins: {
    safety: 10,
    degradation: 25,
  },
};

// Budget allocation by device tier
function getBudgetForDevice(deviceTier: 'high' | 'mid' | 'low'): LatencyBudget {
  const multipliers = {
    high: 1.0,
    mid: 1.5,
    low: 2.5,
  };

  const multiplier = multipliers[deviceTier];

  return {
    ...productionBudget,
    total: productionBudget.total * multiplier,
    phases: Object.fromEntries(
      Object.entries(productionBudget.phases).map(([key, value]) => [
        key,
        Math.round(value * multiplier),
      ])
    ) as PhaseBudgets,
  };
}

// Budget allocation by network condition
function getBudgetForNetwork(
  effectiveType: '4g' | '3g' | '2g' | 'slow-2g'
): LatencyBudget {
  const networkMultipliers = {
    '4g': 1.0,
    '3g': 2.0,
    '2g': 4.0,
    'slow-2g': 8.0,
  };

  const multiplier = networkMultipliers[effectiveType];

  // Network phases scale more than computation phases
  return {
    ...productionBudget,
    total: productionBudget.total * multiplier,
    phases: {
      ...productionBudget.phases,
      dns: productionBudget.phases.dns * multiplier,
      tcp: productionBudget.phases.tcp * multiplier * 1.5,
      tls: productionBudget.phases.tls * multiplier * 1.5,
      ttfb: productionBudget.phases.ttfb * multiplier,
      download: productionBudget.phases.download * multiplier * 2,
    },
  };
}

Budget Distribution Strategy

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Budget Distribution by Page Type                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Landing Page (LCP Focus)                                                   │
│  ─────────────────────────                                                  │
│  Total: 2500ms                                                              │
│                                                                             │
│  ├── Network (40%)         1000ms                                           │
│  │   ├── DNS/TCP/TLS       100ms                                           │
│  │   ├── TTFB              600ms  ◄── Prioritize server response           │
│  │   └── Download          300ms                                           │
│  │                                                                          │
│  ├── Browser (35%)          875ms                                           │
│  │   ├── Parse + Style     200ms                                           │
│  │   ├── Layout + Paint    300ms  ◄── Critical for LCP                     │
│  │   └── LCP Element       375ms                                           │
│  │                                                                          │
│  └── JavaScript (25%)       625ms                                           │
│      ├── Parse + Compile   200ms                                           │
│      ├── Execution         225ms                                           │
│      └── Hydration         200ms  ◄── Defer non-critical                   │
│                                                                             │
│  ═══════════════════════════════════════════════════════════════════════   │
│                                                                             │
│  Dashboard (TTI Focus)                                                      │
│  ─────────────────────                                                      │
│  Total: 3000ms                                                              │
│                                                                             │
│  ├── Network (30%)          900ms                                           │
│  │   ├── DNS/TCP/TLS       100ms                                           │
│  │   ├── TTFB              500ms                                           │
│  │   └── Download          300ms                                           │
│  │                                                                          │
│  ├── Browser (20%)          600ms                                           │
│  │   ├── Parse + Style     200ms                                           │
│  │   └── Layout + Paint    400ms                                           │
│  │                                                                          │
│  └── JavaScript (50%)      1500ms                                           │
│      ├── Parse + Compile   400ms  ◄── More JS for interactivity            │
│      ├── Execution         500ms                                           │
│      └── Hydration         600ms  ◄── Full interactivity required          │
│                                                                             │
│  ═══════════════════════════════════════════════════════════════════════   │
│                                                                             │
│  E-commerce PDP (INP Focus)                                                 │
│  ─────────────────────────────                                              │
│  Total: 2000ms                                                              │
│                                                                             │
│  ├── Network (35%)          700ms                                           │
│  │   ├── DNS/TCP/TLS       100ms                                           │
│  │   ├── TTFB              400ms                                           │
│  │   └── Download          200ms                                           │
│  │                                                                          │
│  ├── Browser (30%)          600ms                                           │
│  │   ├── Parse + Style     150ms                                           │
│  │   └── Layout + Paint    450ms  ◄── Image-heavy                          │
│  │                                                                          │
│  └── JavaScript (35%)       700ms                                           │
│      ├── Parse + Compile   200ms                                           │
│      ├── Execution         200ms                                           │
│      └── Hydration         300ms  ◄── Fast interactions critical           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Server-Side Budget Tracking

Server-Timing Headers

// server/middleware/timing.ts

import { NextRequest, NextResponse } from 'next/server';

interface TimingEntry {
  name: string;
  startTime: number;
  duration?: number;
  description?: string;
}

class ServerTiming {
  private entries: TimingEntry[] = [];
  private startTime: number;

  constructor() {
    this.startTime = performance.now();
  }

  start(name: string, description?: string): () => void {
    const entry: TimingEntry = {
      name,
      startTime: performance.now(),
      description,
    };
    this.entries.push(entry);

    return () => {
      entry.duration = performance.now() - entry.startTime;
    };
  }

  measure(name: string, duration: number, description?: string): void {
    this.entries.push({
      name,
      startTime: performance.now() - duration,
      duration,
      description,
    });
  }

  toHeader(): string {
    return this.entries
      .filter(e => e.duration !== undefined)
      .map(e => {
        const parts = [e.name];
        if (e.duration !== undefined) {
          parts.push(`dur=${e.duration.toFixed(2)}`);
        }
        if (e.description) {
          parts.push(`desc="${e.description}"`);
        }
        return parts.join(';');
      })
      .join(', ');
  }

  getTotalDuration(): number {
    return performance.now() - this.startTime;
  }

  getEntry(name: string): TimingEntry | undefined {
    return this.entries.find(e => e.name === name);
  }
}

// Middleware for timing
export async function timingMiddleware(
  request: NextRequest,
  handler: (req: NextRequest, timing: ServerTiming) => Promise<NextResponse>
): Promise<NextResponse> {
  const timing = new ServerTiming();

  // Track overall request
  const endTotal = timing.start('total', 'Total request time');

  try {
    const response = await handler(request, timing);
    endTotal();

    // Add Server-Timing header
    response.headers.set('Server-Timing', timing.toHeader());

    // Check budget violations
    const budgetViolations = checkBudgetViolations(timing);
    if (budgetViolations.length > 0) {
      response.headers.set('X-Budget-Violations', JSON.stringify(budgetViolations));
    }

    return response;
  } catch (error) {
    endTotal();
    throw error;
  }
}

// Example usage in API route
export async function GET(request: NextRequest) {
  return timingMiddleware(request, async (req, timing) => {
    // Database query
    const endDb = timing.start('db', 'Database query');
    const users = await prisma.user.findMany({ take: 10 });
    endDb();

    // Cache check
    const endCache = timing.start('cache', 'Cache lookup');
    const cached = await redis.get('user-list');
    endCache();

    // Serialization
    const endSerial = timing.start('serial', 'JSON serialization');
    const body = JSON.stringify(users);
    endSerial();

    return NextResponse.json(users);
  });
}

interface BudgetViolation {
  phase: string;
  actual: number;
  budget: number;
  severity: 'warning' | 'critical';
}

function checkBudgetViolations(timing: ServerTiming): BudgetViolation[] {
  const violations: BudgetViolation[] = [];

  const budgets: Record<string, { budget: number; critical: number }> = {
    db: { budget: 200, critical: 500 },
    cache: { budget: 10, critical: 50 },
    serial: { budget: 50, critical: 100 },
    total: { budget: 400, critical: 800 },
  };

  for (const [name, thresholds] of Object.entries(budgets)) {
    const entry = timing.getEntry(name);
    if (entry?.duration) {
      if (entry.duration > thresholds.critical) {
        violations.push({
          phase: name,
          actual: entry.duration,
          budget: thresholds.budget,
          severity: 'critical',
        });
      } else if (entry.duration > thresholds.budget) {
        violations.push({
          phase: name,
          actual: entry.duration,
          budget: thresholds.budget,
          severity: 'warning',
        });
      }
    }
  }

  return violations;
}

Edge Function Timing

// edge/timing-edge.ts (Cloudflare Worker)

interface EdgeTiming {
  edgeReceive: number;
  cacheCheck: number;
  originFetch: number;
  transform: number;
  edgeSend: number;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const timing: EdgeTiming = {
      edgeReceive: 0,
      cacheCheck: 0,
      originFetch: 0,
      transform: 0,
      edgeSend: 0,
    };

    const requestStart = Date.now();

    // Edge receive time (time to receive full request)
    timing.edgeReceive = Date.now() - requestStart;

    // Cache check
    const cacheStart = Date.now();
    const cache = caches.default;
    const cacheKey = new Request(request.url, request);
    let response = await cache.match(cacheKey);
    timing.cacheCheck = Date.now() - cacheStart;

    if (!response) {
      // Origin fetch
      const originStart = Date.now();
      response = await fetch(request);
      timing.originFetch = Date.now() - originStart;

      // Transform response
      const transformStart = Date.now();
      response = await transformResponse(response);
      timing.transform = Date.now() - transformStart;

      // Cache the response
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }

    // Calculate total edge time
    timing.edgeSend = Date.now() - requestStart;

    // Add timing headers
    const headers = new Headers(response.headers);
    headers.set('Server-Timing', formatEdgeTiming(timing));
    headers.set('X-Edge-Cache', response ? 'HIT' : 'MISS');
    headers.set('X-Edge-Location', request.cf?.colo as string);

    return new Response(response.body, {
      status: response.status,
      headers,
    });
  },
};

function formatEdgeTiming(timing: EdgeTiming): string {
  return Object.entries(timing)
    .map(([name, duration]) => `${name};dur=${duration}`)
    .join(', ');
}

async function transformResponse(response: Response): Promise<Response> {
  // Example: inject timing beacon
  if (response.headers.get('content-type')?.includes('text/html')) {
    const html = await response.text();
    const injected = html.replace(
      '</head>',
      `<script>window.__EDGE_TIMING__=${Date.now()}</script></head>`
    );
    return new Response(injected, {
      status: response.status,
      headers: response.headers,
    });
  }
  return response;
}

Client-Side Budget Monitoring

Performance Observer Integration

// src/performance/monitor.ts

interface PerformanceMetrics {
  // Navigation timing
  dns: number;
  tcp: number;
  tls: number;
  ttfb: number;
  download: number;

  // Paint timing
  fcp: number;
  lcp: number;

  // Interaction timing
  fid: number;
  inp: number;

  // Custom timing
  hydration: number;
  dataFetch: number;

  // Server timing (from headers)
  serverDb: number;
  serverCache: number;
  serverTotal: number;
}

interface BudgetConfig {
  budgets: Record<keyof PerformanceMetrics, number>;
  onViolation: (violations: BudgetViolation[]) => void;
  sampleRate: number;
}

class PerformanceBudgetMonitor {
  private metrics: Partial<PerformanceMetrics> = {};
  private observers: PerformanceObserver[] = [];
  private hydrationStart: number = 0;

  constructor(private config: BudgetConfig) {
    this.setupObservers();
  }

  private setupObservers() {
    // Navigation timing
    this.observeNavigation();

    // Paint timing
    this.observePaint();

    // Long tasks
    this.observeLongTasks();

    // Layout shifts
    this.observeLayoutShifts();

    // First input
    this.observeFirstInput();

    // Event timing (INP)
    this.observeEventTiming();
  }

  private observeNavigation() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries() as PerformanceNavigationTiming[];
      const nav = entries[0];

      if (nav) {
        this.metrics.dns = nav.domainLookupEnd - nav.domainLookupStart;
        this.metrics.tcp = nav.connectEnd - nav.connectStart;
        this.metrics.tls = nav.secureConnectionStart > 0
          ? nav.connectEnd - nav.secureConnectionStart
          : 0;
        this.metrics.ttfb = nav.responseStart - nav.requestStart;
        this.metrics.download = nav.responseEnd - nav.responseStart;

        // Parse server timing from response
        this.parseServerTiming();

        this.checkBudgets();
      }
    });

    observer.observe({ type: 'navigation', buffered: true });
    this.observers.push(observer);
  }

  private observePaint() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
          this.metrics.fcp = entry.startTime;
        }
      }
      this.checkBudgets();
    });

    observer.observe({ type: 'paint', buffered: true });
    this.observers.push(observer);

    // LCP observer
    const lcpObserver = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.metrics.lcp = lastEntry.startTime;
      this.checkBudgets();
    });

    lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
    this.observers.push(lcpObserver);
  }

  private observeFirstInput() {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries() as PerformanceEventTiming[];
      const firstInput = entries[0];

      if (firstInput) {
        this.metrics.fid = firstInput.processingStart - firstInput.startTime;
        this.checkBudgets();
      }
    });

    observer.observe({ type: 'first-input', buffered: true });
    this.observers.push(observer);
  }

  private observeEventTiming() {
    let maxINP = 0;

    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries() as PerformanceEventTiming[]) {
        // INP considers interactions, not all events
        if (['pointerdown', 'pointerup', 'keydown', 'keyup', 'click'].includes(entry.name)) {
          const duration = entry.duration;
          if (duration > maxINP) {
            maxINP = duration;
            this.metrics.inp = duration;
            this.checkBudgets();
          }
        }
      }
    });

    observer.observe({ type: 'event', buffered: true });
    this.observers.push(observer);
  }

  private observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // Log long tasks that may impact interactivity
        if (entry.duration > 50) {
          console.warn(`Long task detected: ${entry.duration}ms`, entry);
        }
      }
    });

    observer.observe({ type: 'longtask', buffered: true });
    this.observers.push(observer);
  }

  private observeLayoutShifts() {
    let clsValue = 0;

    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const shift = entry as PerformanceEntry & { value: number; hadRecentInput: boolean };
        if (!shift.hadRecentInput) {
          clsValue += shift.value;
        }
      }
    });

    observer.observe({ type: 'layout-shift', buffered: true });
    this.observers.push(observer);
  }

  private parseServerTiming() {
    const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;

    if (nav?.serverTiming) {
      for (const timing of nav.serverTiming) {
        switch (timing.name) {
          case 'db':
            this.metrics.serverDb = timing.duration;
            break;
          case 'cache':
            this.metrics.serverCache = timing.duration;
            break;
          case 'total':
            this.metrics.serverTotal = timing.duration;
            break;
        }
      }
    }
  }

  // Mark hydration timing
  markHydrationStart() {
    this.hydrationStart = performance.now();
  }

  markHydrationEnd() {
    if (this.hydrationStart > 0) {
      this.metrics.hydration = performance.now() - this.hydrationStart;
      this.checkBudgets();
    }
  }

  // Mark custom timing
  markDataFetch(duration: number) {
    this.metrics.dataFetch = duration;
    this.checkBudgets();
  }

  private checkBudgets() {
    // Sample to reduce overhead
    if (Math.random() > this.config.sampleRate) return;

    const violations: BudgetViolation[] = [];

    for (const [metric, value] of Object.entries(this.metrics)) {
      const budget = this.config.budgets[metric as keyof PerformanceMetrics];

      if (budget && value && value > budget) {
        violations.push({
          phase: metric,
          actual: value,
          budget,
          severity: value > budget * 2 ? 'critical' : 'warning',
        });
      }
    }

    if (violations.length > 0) {
      this.config.onViolation(violations);
    }
  }

  getMetrics(): Partial<PerformanceMetrics> {
    return { ...this.metrics };
  }

  disconnect() {
    for (const observer of this.observers) {
      observer.disconnect();
    }
  }
}

// Initialize monitor
const budgetMonitor = new PerformanceBudgetMonitor({
  budgets: {
    dns: 50,
    tcp: 50,
    tls: 100,
    ttfb: 600,
    download: 200,
    fcp: 1800,
    lcp: 2500,
    fid: 100,
    inp: 200,
    hydration: 300,
    dataFetch: 500,
    serverDb: 200,
    serverCache: 20,
    serverTotal: 400,
  },
  onViolation: (violations) => {
    // Report to analytics
    violations.forEach(v => {
      console.warn(`Budget violation: ${v.phase} took ${v.actual}ms (budget: ${v.budget}ms)`);

      // Send to monitoring
      fetch('/api/telemetry/budget-violation', {
        method: 'POST',
        body: JSON.stringify(v),
        keepalive: true,
      });
    });
  },
  sampleRate: 0.1, // 10% sampling
});

export { budgetMonitor, PerformanceBudgetMonitor };

Hydration Budget Strategies

Progressive Hydration with Budget

// src/hydration/budgeted-hydration.ts

interface HydrationTask {
  id: string;
  priority: 'critical' | 'high' | 'medium' | 'low';
  estimatedCost: number; // ms
  hydrate: () => Promise<void>;
}

interface HydrationSchedulerConfig {
  frameBudget: number;      // Max ms per frame (default: 10)
  totalBudget: number;      // Total hydration budget (default: 500)
  idleTimeout: number;      // requestIdleCallback timeout
}

class BudgetedHydrationScheduler {
  private queue: HydrationTask[] = [];
  private isProcessing = false;
  private totalTimeSpent = 0;
  private completedTasks: string[] = [];

  constructor(private config: HydrationSchedulerConfig = {
    frameBudget: 10,
    totalBudget: 500,
    idleTimeout: 2000,
  }) {}

  schedule(task: HydrationTask): void {
    // Insert by priority
    const insertIndex = this.queue.findIndex(t =>
      this.getPriorityValue(t.priority) < this.getPriorityValue(task.priority)
    );

    if (insertIndex === -1) {
      this.queue.push(task);
    } else {
      this.queue.splice(insertIndex, 0, task);
    }

    this.startProcessing();
  }

  private getPriorityValue(priority: HydrationTask['priority']): number {
    const values = { critical: 4, high: 3, medium: 2, low: 1 };
    return values[priority];
  }

  private startProcessing(): void {
    if (this.isProcessing) return;
    this.isProcessing = true;
    this.processQueue();
  }

  private processQueue(): void {
    if (this.queue.length === 0) {
      this.isProcessing = false;
      return;
    }

    // Use requestIdleCallback for non-critical tasks
    const nextTask = this.queue[0];

    if (nextTask.priority === 'critical' || nextTask.priority === 'high') {
      // Process immediately but yield to main thread
      this.processWithYield();
    } else {
      // Use idle callback
      requestIdleCallback(
        (deadline) => this.processInIdleTime(deadline),
        { timeout: this.config.idleTimeout }
      );
    }
  }

  private async processWithYield(): Promise<void> {
    while (this.queue.length > 0) {
      const task = this.queue[0];

      // Check if we've exceeded total budget
      if (this.totalTimeSpent >= this.config.totalBudget) {
        console.warn('Hydration budget exceeded, deferring remaining tasks');
        this.deferRemainingTasks();
        return;
      }

      const start = performance.now();

      try {
        await task.hydrate();
        const duration = performance.now() - start;
        this.totalTimeSpent += duration;
        this.completedTasks.push(task.id);

        // Report timing
        this.reportTaskTiming(task, duration);

      } catch (error) {
        console.error(`Hydration failed for ${task.id}:`, error);
      }

      this.queue.shift();

      // Yield to main thread every frame budget
      if (performance.now() - start > this.config.frameBudget) {
        await this.yieldToMainThread();
      }
    }

    this.isProcessing = false;
  }

  private processInIdleTime(deadline: IdleDeadline): void {
    while (
      this.queue.length > 0 &&
      deadline.timeRemaining() > 0 &&
      this.totalTimeSpent < this.config.totalBudget
    ) {
      const task = this.queue.shift()!;
      const start = performance.now();

      try {
        // Synchronous hydration for idle time
        // Note: This is simplified; real implementation would handle async
        task.hydrate();
        const duration = performance.now() - start;
        this.totalTimeSpent += duration;
        this.completedTasks.push(task.id);
        this.reportTaskTiming(task, duration);

      } catch (error) {
        console.error(`Hydration failed for ${task.id}:`, error);
      }
    }

    if (this.queue.length > 0) {
      requestIdleCallback(
        (deadline) => this.processInIdleTime(deadline),
        { timeout: this.config.idleTimeout }
      );
    } else {
      this.isProcessing = false;
    }
  }

  private yieldToMainThread(): Promise<void> {
    return new Promise(resolve => {
      // Use scheduler.yield() if available, otherwise setTimeout
      if ('scheduler' in globalThis && 'yield' in (globalThis as any).scheduler) {
        (globalThis as any).scheduler.yield().then(resolve);
      } else {
        setTimeout(resolve, 0);
      }
    });
  }

  private deferRemainingTasks(): void {
    // Move remaining tasks to be hydrated on interaction
    for (const task of this.queue) {
      this.setupInteractionHydration(task);
    }
    this.queue = [];
    this.isProcessing = false;
  }

  private setupInteractionHydration(task: HydrationTask): void {
    const element = document.getElementById(task.id);
    if (!element) return;

    const hydrate = () => {
      task.hydrate();
      element.removeEventListener('click', hydrate);
      element.removeEventListener('focus', hydrate);
      element.removeEventListener('mouseenter', hydrate);
    };

    element.addEventListener('click', hydrate, { once: true });
    element.addEventListener('focus', hydrate, { once: true });
    element.addEventListener('mouseenter', hydrate, { once: true });
  }

  private reportTaskTiming(task: HydrationTask, duration: number): void {
    // Report to performance monitoring
    performance.measure(`hydration:${task.id}`, {
      start: performance.now() - duration,
      duration,
    });
  }

  getStats(): {
    completed: number;
    pending: number;
    totalTime: number;
    budgetRemaining: number;
  } {
    return {
      completed: this.completedTasks.length,
      pending: this.queue.length,
      totalTime: this.totalTimeSpent,
      budgetRemaining: Math.max(0, this.config.totalBudget - this.totalTimeSpent),
    };
  }
}

export const hydrationScheduler = new BudgetedHydrationScheduler();

// React integration
function useBudgetedHydration(
  id: string,
  priority: HydrationTask['priority'] = 'medium'
) {
  const [isHydrated, setIsHydrated] = useState(false);

  useEffect(() => {
    const estimatedCost = estimateHydrationCost(id);

    hydrationScheduler.schedule({
      id,
      priority,
      estimatedCost,
      hydrate: async () => {
        setIsHydrated(true);
      },
    });
  }, [id, priority]);

  return isHydrated;
}

function estimateHydrationCost(componentId: string): number {
  // Estimate based on DOM complexity
  const element = document.getElementById(componentId);
  if (!element) return 50;

  const nodeCount = element.querySelectorAll('*').length;
  const hasEventHandlers = element.querySelectorAll('[data-event]').length;

  return Math.min(500, nodeCount * 2 + hasEventHandlers * 10);
}

export { useBudgetedHydration, BudgetedHydrationScheduler };

Budget Visualization and Debugging

Performance Budget Dashboard

// src/performance/budget-dashboard.ts

interface BudgetDashboardData {
  timestamp: number;
  metrics: Record<string, number>;
  budgets: Record<string, number>;
  violations: BudgetViolation[];
  percentiles: Record<string, { p50: number; p75: number; p95: number; p99: number }>;
}

class BudgetDashboard {
  private canvas: HTMLCanvasElement | null = null;
  private ctx: CanvasRenderingContext2D | null = null;
  private data: BudgetDashboardData | null = null;

  mount(container: HTMLElement): void {
    this.canvas = document.createElement('canvas');
    this.canvas.width = 800;
    this.canvas.height = 600;
    this.canvas.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      background: rgba(0, 0, 0, 0.9);
      border-radius: 8px;
      z-index: 99999;
    `;
    container.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d');

    this.startMonitoring();
  }

  private startMonitoring(): void {
    setInterval(() => this.updateData(), 1000);
  }

  private updateData(): void {
    const metrics = budgetMonitor.getMetrics();
    const budgets = productionBudget.phases;

    this.data = {
      timestamp: Date.now(),
      metrics: metrics as Record<string, number>,
      budgets: budgets as unknown as Record<string, number>,
      violations: this.checkViolations(metrics, budgets),
      percentiles: this.calculatePercentiles(),
    };

    this.render();
  }

  private checkViolations(
    metrics: Record<string, number>,
    budgets: Record<string, number>
  ): BudgetViolation[] {
    const violations: BudgetViolation[] = [];

    for (const [key, value] of Object.entries(metrics)) {
      const budget = budgets[key];
      if (budget && value > budget) {
        violations.push({
          phase: key,
          actual: value,
          budget,
          severity: value > budget * 2 ? 'critical' : 'warning',
        });
      }
    }

    return violations;
  }

  private calculatePercentiles(): Record<string, { p50: number; p75: number; p95: number; p99: number }> {
    // This would aggregate from stored metrics
    return {};
  }

  private render(): void {
    if (!this.ctx || !this.data) return;

    const ctx = this.ctx;
    const { metrics, budgets, violations } = this.data;

    // Clear canvas
    ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
    ctx.fillRect(0, 0, 800, 600);

    // Title
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 18px monospace';
    ctx.fillText('Performance Budget Dashboard', 20, 30);

    // Draw waterfall
    this.drawWaterfall(ctx, metrics, budgets, 20, 60, 760, 200);

    // Draw budget bars
    this.drawBudgetBars(ctx, metrics, budgets, 20, 280, 760, 150);

    // Draw violations
    this.drawViolations(ctx, violations, 20, 450, 760, 130);
  }

  private drawWaterfall(
    ctx: CanvasRenderingContext2D,
    metrics: Record<string, number>,
    budgets: Record<string, number>,
    x: number,
    y: number,
    width: number,
    height: number
  ): void {
    const phases = ['dns', 'tcp', 'tls', 'ttfb', 'download', 'fcp', 'lcp', 'hydration'];
    const barHeight = height / phases.length - 5;
    const maxTime = Math.max(...Object.values(metrics), 3000);

    ctx.fillStyle = '#333';
    ctx.fillRect(x, y, width, height);

    phases.forEach((phase, i) => {
      const value = metrics[phase] || 0;
      const budget = budgets[phase] || 0;
      const barY = y + i * (barHeight + 5);
      const barWidth = (value / maxTime) * width;
      const budgetX = x + (budget / maxTime) * width;

      // Draw bar
      ctx.fillStyle = value > budget ? '#e74c3c' : '#2ecc71';
      ctx.fillRect(x, barY, barWidth, barHeight);

      // Draw budget line
      ctx.strokeStyle = '#f39c12';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(budgetX, barY);
      ctx.lineTo(budgetX, barY + barHeight);
      ctx.stroke();

      // Draw label
      ctx.fillStyle = '#fff';
      ctx.font = '12px monospace';
      ctx.fillText(`${phase}: ${value.toFixed(0)}ms`, x + 5, barY + barHeight - 5);
    });
  }

  private drawBudgetBars(
    ctx: CanvasRenderingContext2D,
    metrics: Record<string, number>,
    budgets: Record<string, number>,
    x: number,
    y: number,
    width: number,
    height: number
  ): void {
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 14px monospace';
    ctx.fillText('Budget Utilization', x, y - 10);

    const categories = [
      { name: 'Network', phases: ['dns', 'tcp', 'tls', 'ttfb', 'download'] },
      { name: 'Browser', phases: ['fcp', 'lcp'] },
      { name: 'JavaScript', phases: ['hydration'] },
    ];

    const barWidth = (width - 40) / categories.length;

    categories.forEach((category, i) => {
      const catX = x + i * (barWidth + 20);
      const totalBudget = category.phases.reduce((sum, p) => sum + (budgets[p] || 0), 0);
      const totalActual = category.phases.reduce((sum, p) => sum + (metrics[p] || 0), 0);
      const utilization = totalBudget > 0 ? (totalActual / totalBudget) * 100 : 0;

      // Background
      ctx.fillStyle = '#333';
      ctx.fillRect(catX, y, barWidth, height - 30);

      // Utilization bar
      const barHeight = Math.min((utilization / 100) * (height - 30), height - 30);
      ctx.fillStyle = utilization > 100 ? '#e74c3c' : utilization > 80 ? '#f39c12' : '#2ecc71';
      ctx.fillRect(catX, y + height - 30 - barHeight, barWidth, barHeight);

      // Label
      ctx.fillStyle = '#fff';
      ctx.font = '11px monospace';
      ctx.fillText(category.name, catX, y + height - 10);
      ctx.fillText(`${utilization.toFixed(0)}%`, catX, y + height + 5);
    });
  }

  private drawViolations(
    ctx: CanvasRenderingContext2D,
    violations: BudgetViolation[],
    x: number,
    y: number,
    width: number,
    height: number
  ): void {
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 14px monospace';
    ctx.fillText(`Violations (${violations.length})`, x, y - 10);

    if (violations.length === 0) {
      ctx.fillStyle = '#2ecc71';
      ctx.font = '14px monospace';
      ctx.fillText('All metrics within budget', x, y + 20);
      return;
    }

    violations.slice(0, 5).forEach((v, i) => {
      const vY = y + i * 22;
      ctx.fillStyle = v.severity === 'critical' ? '#e74c3c' : '#f39c12';
      ctx.font = '12px monospace';
      ctx.fillText(
        `${v.severity.toUpperCase()}: ${v.phase} - ${v.actual.toFixed(0)}ms (budget: ${v.budget}ms)`,
        x,
        vY + 15
      );
    });
  }

  unmount(): void {
    this.canvas?.remove();
  }
}

// Enable in development
if (process.env.NODE_ENV === 'development') {
  const dashboard = new BudgetDashboard();
  dashboard.mount(document.body);
}

export { BudgetDashboard };

Key Takeaways

  1. Budgets are architectural constraints: Define performance targets before implementation, not after

  2. Allocate by phase, not total: Break down the total budget into DNS, TLS, TTFB, rendering, hydration components

  3. Device and network tiers need different budgets: A 3G user on a low-end phone has a different reality than fiber + MacBook

  4. Server-Timing headers enable visibility: Propagate backend timing to the client for full-stack analysis

  5. Hydration often dominates TTI: Progressive and budgeted hydration prevents JavaScript from consuming the entire budget

  6. Monitor actual vs budget continuously: Performance Observer APIs enable real-time budget tracking

  7. Violations should trigger alerts: Integrate budget monitoring with your incident response system

  8. Yield to main thread: Long tasks that exceed frame budget harm interactivity regardless of total time

  9. Sample strategically: 100% monitoring is expensive; sample more from slow sessions

  10. Make budgets visible: Dashboards and dev tools help teams understand their performance impact

Performance budgets transform vague performance goals into concrete, measurable architectural constraints that guide design decisions throughout development.

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.