Module: JavaScript
JavaScript·085·4 MIN READ

085: Browser DevTools, Debugging, and Performance

TOPICS COVERED: Browser DevTools, Debugging, and Performance

Outcomes

By the end of this lesson, you can:

  • debug JavaScript systematically instead of relying only on console.log;
  • use breakpoints, scopes, call stacks, network inspection, and performance tools;
  • distinguish functional bugs from performance problems;
  • identify long tasks and expensive DOM work;
  • use debounce, throttle, batching, and scheduling appropriately;
  • investigate memory retention with browser tools.

A Debugging Process

Use a repeatable sequence:

  1. reproduce the bug;
  2. minimize the conditions;
  3. state expected versus actual behavior;
  4. inspect inputs and state;
  5. pause execution at the right boundary;
  6. trace the call stack;
  7. test one hypothesis at a time;
  8. add a regression test after fixing.

Random edits are not debugging.

Breakpoints

Use source breakpoints to pause before the suspicious line.

While paused, inspect:

  • local variables;
  • closure scope;
  • this;
  • call stack;
  • expressions in the console.

Conditional breakpoints reduce noise in loops:

js
// conceptual condition:
order.id === "ORD-8472"

debugger

js
function calculate(order) {
  debugger;
  return order.items.reduce(
    (sum, item) => sum + item.price,
    0
  );
}

When DevTools is open, debugger pauses execution. Remove accidental debugger statements before production.

Call Stack

If an error originates deep in a helper, the stack shows the path that called it.

js
function checkout() {
  validate();
}

function validate() {
  throw new Error("Invalid order");
}

checkout();

Read from the error frame outward.

Network Panel

For API bugs, inspect:

  • request URL;
  • method;
  • headers;
  • request body;
  • status;
  • response body;
  • timing;
  • redirects;
  • CORS/preflight behavior.

Do not guess at network failures from UI symptoms alone.

Long Tasks

JavaScript on the main thread competes with rendering and input handling.

js
const start = performance.now();

doExpensiveWork();

console.log(performance.now() - start);

performance.now() is suitable for high-resolution duration measurement.

A single measurement is not a benchmark, but it is useful during investigation.

Avoid Layout Thrashing

This pattern can repeatedly force layout:

js
for (const card of cards) {
  card.style.width =
    `${container.getBoundingClientRect().width / 3}px`;
}

Read measurements together, then write updates together.

js
const width = container.getBoundingClientRect().width;
const cardWidth = width / 3;

for (const card of cards) {
  card.style.width = `${cardWidth}px`;
}

Prefer CSS layout when the problem is purely layout.

Debounce

Debounce waits for activity to settle.

js
function debounce(fn, delay) {
  let timeoutId;

  return (...args) => {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

Good for search input where you want one request after typing pauses.

Throttle

Throttle limits execution frequency during continuous events.

js
function throttle(fn, interval) {
  let lastRun = 0;

  return (...args) => {
    const now = Date.now();

    if (now - lastRun >= interval) {
      lastRun = now;
      fn(...args);
    }
  };
}

There are more robust implementations with trailing calls and cancellation. Understand the policy before choosing a utility.

requestAnimationFrame

For visual updates tied to rendering:

js
requestAnimationFrame(() => {
  element.style.transform = "translateX(20px)";
});

It schedules work before a future paint opportunity.

requestIdleCallback

Where supported, nonurgent work can sometimes use idle time, but it is not universally available and should not be required for critical logic.

Use feature detection/fallbacks when appropriate.

Performance API

Marks/measures:

js
performance.mark("render-start");

render();

performance.mark("render-end");

performance.measure(
  "render",
  "render-start",
  "render-end"
);

console.log(performance.getEntriesByName("render"));

Memory Tools

Heap snapshots can help identify:

  • detached DOM nodes;
  • retaining paths;
  • unexpectedly growing arrays/maps;
  • objects kept alive by closures/listeners.

Compare behavior across repeated actions instead of interpreting one snapshot in isolation.

Worked Debugging Example: Duplicate Request

Symptom: one button click sends two POST requests.

Investigation:

  1. Network panel confirms two identical requests.
  2. Event-listener breakpoint shows handler runs twice.
  3. Call stack reveals mount() is executed twice.
  4. Inspect listener lifecycle.
  5. Fix mount/destroy semantics.
  6. Add test that one click calls the API once.

This turns a vague "API bug" into a concrete listener-lifecycle bug.

Performance versus Correctness

Do not optimize incorrect code.

Priorities:

  1. correct;
  2. clear;
  3. measured;
  4. optimized where the measurement justifies it.

Micro-optimizing array syntax while rendering thousands of unnecessary DOM nodes misses the real bottleneck.

Deep Performance: Complexity, Rendering, and Scheduling

Performance problems can occur at different layers:

  • algorithmic complexity;
  • network latency;
  • large payloads;
  • excessive parsing;
  • expensive layout/paint;
  • too many DOM nodes;
  • repeated event work;
  • memory pressure;
  • unnecessary framework/component rendering.

Do not jump directly to micro-benchmarks.

Algorithmic example

This performs repeated linear searches:

js
const enriched = orders.map((order) => ({
  ...order,
  customer: customers.find(
    (customer) => customer.id === order.customerId
  ),
}));

For large collections, build an index once:

js
const customerById = new Map(
  customers.map((customer) => [
    customer.id,
    customer,
  ])
);

const enriched = orders.map((order) => ({
  ...order,
  customer: customerById.get(order.customerId),
}));

PerformanceObserver preview

The browser can expose performance entries to JavaScript.

js
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.entryType, entry.duration);
  }
});

Which entry types are available depends on the browser. This is an observability tool, not a replacement for DevTools profiling.

Build a performance hypothesis

Write:

"Rendering is slow because each row performs a repeated querySelector across the whole document."

Then measure it. A falsifiable hypothesis prevents vague optimization work.

Best Practices

  • Reproduce before editing.
  • Use breakpoints and stack traces.
  • Inspect actual HTTP traffic.
  • Measure performance before optimizing.
  • Prefer CSS/layout/platform APIs over heavy JavaScript where possible.
  • Keep event handlers small.
  • Batch DOM reads and writes.
  • Add cleanup for listeners, timers, observers, and subscriptions.

Exercises

Core

Use a breakpoint to inspect a function parameter.

Practice

Debounce a search input and verify in the Network panel that rapid typing produces one final request.

Professional Extension

Profile a render function, identify the slowest operation, change one thing, and compare before/after measurements.

Recap

DevTools is part of JavaScript development, not an emergency tool. Good debugging and performance work is evidence-driven.