Skip to main content
fast frontend

Chrome DevTools Performance Traces in Practice

5 min read Chapter 2 of 33

Chrome DevTools Performance Traces in Practice

The Symptom

The e-commerce checkout page has a p75 INP of 320ms. Users clicking “Apply Coupon” see a 400ms delay before the UI updates. The field data shows the problem. Chrome DevTools Performance panel shows the cause.

The Cause

The “Apply Coupon” click handler triggers a synchronous state update that forces a re-render of the entire order summary component tree. The re-render includes a price recalculation function that iterates over all cart items, applies discount rules, computes tax, and formats currency strings. This runs on the main thread as a single long task. While it runs, the browser cannot paint the next frame.

The mechanical sequence:

  1. User clicks “Apply Coupon.”
  2. Browser dispatches the click event to the React event handler.
  3. The handler calls setState with the new coupon code.
  4. React schedules a re-render of the OrderSummary component.
  5. During re-render, the calculateTotal function runs synchronously.
  6. calculateTotal iterates 47 line items, applies 3 discount rule checks per item, computes tax rates for 2 jurisdictions.
  7. React completes the virtual DOM diff.
  8. React commits DOM updates.
  9. Browser paints the next frame.

Steps 4 through 8 execute as a single task measured at 380ms on a 4x CPU-throttled profile. The 50ms long task threshold is exceeded by 330ms.

The Baseline

Recording with 4x CPU throttle and Slow 3G network:

  • Click event fires at t=0ms
  • React setState enqueued at t=2ms
  • calculateTotal begins at t=8ms
  • calculateTotal completes at t=285ms
  • Virtual DOM diff completes at t=340ms
  • DOM commit at t=358ms
  • Paint at t=380ms
  • INP for this interaction: 380ms

The Performance panel flame chart shows calculateTotal as the widest block within the task, consuming 277ms of the 380ms total. The Call Tree view confirms: calculateTotal > applyDiscountRules > evaluateRule accounts for 72% of the task duration.

The Fix

Split the computation off the critical rendering path. The coupon application can update the UI optimistically (show the coupon as applied) and compute the exact total asynchronously.

// SLOW: Synchronous total recalculation blocks the main thread
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
  taxRate: number;
}

interface DiscountRule {
  type: "percentage" | "fixed" | "bogo";
  value: number;
  minQuantity: number;
  applicableCategories: string[];
}

function handleApplyCoupon(couponCode: string): void {
  const rules = fetchDiscountRules(couponCode);
  const total = calculateTotal(cartItems, rules); // 277ms on throttled CPU
  setState({ couponApplied: true, total, rules });
}

// FAST: Optimistic UI update, deferred computation
function handleApplyCoupon(couponCode: string): void {
  // Immediate UI feedback: show coupon badge, disable button
  setState({ couponApplied: true, calculating: true });

  // Defer expensive computation to next idle period
  requestIdleCallback(() => {
    const rules = fetchDiscountRules(couponCode);
    const total = calculateTotal(cartItems, rules);
    setState({ total, rules, calculating: false });
  });
}

For the calculateTotal function itself, the 277ms includes redundant work. Each item’s discount eligibility is checked against all rules, but most rules apply to zero items. Pre-filtering rules by category reduces iterations.

// SLOW: O(items * rules * categories) for every recalculation
function calculateTotal(items: CartItem[], rules: DiscountRule[]): number {
  let total = 0;
  for (const item of items) {
    let itemTotal = item.price * item.quantity;
    for (const rule of rules) {
      for (const category of rule.applicableCategories) {
        if (getItemCategory(item.id) === category) {
          itemTotal = applyRule(itemTotal, rule);
        }
      }
    }
    total += itemTotal * (1 + item.taxRate);
  }
  return total;
}

// FAST: Pre-index rules by category, skip inapplicable rules
function calculateTotal(items: CartItem[], rules: DiscountRule[]): number {
  const rulesByCategory = new Map<string, DiscountRule[]>();
  for (const rule of rules) {
    for (const cat of rule.applicableCategories) {
      const existing = rulesByCategory.get(cat) ?? [];
      existing.push(rule);
      rulesByCategory.set(cat, existing);
    }
  }

  let total = 0;
  for (const item of items) {
    let itemTotal = item.price * item.quantity;
    const category = getItemCategory(item.id);
    const applicableRules = rulesByCategory.get(category) ?? [];
    for (const rule of applicableRules) {
      itemTotal = applyRule(itemTotal, rule);
    }
    total += itemTotal * (1 + item.taxRate);
  }
  return total;
}

The Proof

After both changes, re-recording the performance trace with the same throttling:

  • Click event fires at t=0ms
  • React setState (optimistic) enqueued at t=2ms
  • DOM commit (coupon badge) at t=18ms
  • Paint at t=22ms
  • INP for this interaction: 22ms
  • requestIdleCallback fires calculateTotal at t=45ms
  • calculateTotal completes at t=89ms (down from 277ms with pre-indexed rules)
  • Second paint with final total at t=105ms

INP improved from 380ms to 22ms. The user sees immediate feedback. The final total appears 105ms after the click, which is below the 200ms “good” threshold for perceived responsiveness.

The Trade-off

Optimistic UI updates introduce a brief period where the displayed total is stale. If the discount computation reveals an error (invalid coupon, expired rule), you must handle the rollback gracefully. The calculating: true state provides a loading indicator, but the UX cost is a flicker of “Calculating…” text for ~80ms on fast devices. On the e-commerce platform, user testing showed no measurable impact on checkout completion rate from this flicker.

The pre-indexed rule lookup trades memory for CPU time. For the typical case of 3-5 discount rules and 10-20 categories, the Map overhead is negligible. For a hypothetical system with thousands of rules, the index construction itself becomes measurable. That system has bigger problems than frontend performance.

The CI performance gate from Chapter 2 catches this regression pattern: any interaction that produces a long task over 200ms on a throttled profile triggers a warning. Over 350ms blocks the merge.