Chrome DevTools Performance Traces in Practice
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:
- User clicks “Apply Coupon.”
- Browser dispatches the click event to the React event handler.
- The handler calls
setStatewith the new coupon code. - React schedules a re-render of the
OrderSummarycomponent. - During re-render, the
calculateTotalfunction runs synchronously. calculateTotaliterates 47 line items, applies 3 discount rule checks per item, computes tax rates for 2 jurisdictions.- React completes the virtual DOM diff.
- React commits DOM updates.
- 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
setStateenqueued at t=2ms calculateTotalbegins at t=8mscalculateTotalcompletes 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
requestIdleCallbackfirescalculateTotalat t=45mscalculateTotalcompletes 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.