085: 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:
- reproduce the bug;
- minimize the conditions;
- state expected versus actual behavior;
- inspect inputs and state;
- pause execution at the right boundary;
- trace the call stack;
- test one hypothesis at a time;
- 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:
// conceptual condition:
order.id === "ORD-8472"
debugger
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.
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.
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:
for (const card of cards) {
card.style.width =
`${container.getBoundingClientRect().width / 3}px`;
}
Read measurements together, then write updates together.
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.
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.
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:
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:
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:
- Network panel confirms two identical requests.
- Event-listener breakpoint shows handler runs twice.
- Call stack reveals
mount()is executed twice. - Inspect listener lifecycle.
- Fix mount/destroy semantics.
- 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:
- correct;
- clear;
- measured;
- 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:
const enriched = orders.map((order) => ({
...order,
customer: customers.find(
(customer) => customer.id === order.customerId
),
}));
For large collections, build an index once:
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.
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.
