Browser DevTools โ Way Beyond console.log
The debugging features hiding in plain sight that took me years to discover. Performance profiling, memory leak hunting, network simulation, and the snippets panel I now use daily.

For the first two years of my web development career, my debugging toolkit was console.log. Sprinkle a few logs, check the console, add more logs, narrow down the problem. Effective, eventually. Also slow, messy, and embarrassing when you accidentally ship a console.log('HERE HERE HERE') to production.
The turning point was watching a senior developer debug a performance issue in about three minutes using the Performance tab. She recorded a trace, found the function causing a 200ms layout thrash, fixed it, recorded again to confirm. No console.logs. No guessing. I'd been staring at the same issue for an hour adding log statements to narrow down which component was slow.
Browser DevTools are absurdly powerful. Most developers โ myself included for a while โ probably use maybe 10% of what's available. I'm going to walk through the features that changed how I debug, ordered by how often I actually use them.
Console โ Actually Using It Properly
Before moving beyond the console, let's at least use it well. console.log is the blunt instrument. There are sharper tools in the same drawer.
console.table for Arrays and Objects
const users = [
{ name: 'Anurag', role: 'admin', lastLogin: '2026-03-20' },
{ name: 'Sarah', role: 'editor', lastLogin: '2026-03-19' },
{ name: 'Mike', role: 'viewer', lastLogin: '2026-03-15' },
];
console.table(users);
Renders a sortable, formatted table in the console. Clicking column headers sorts by that column. For arrays of objects, this is dramatically more readable than the collapsed object view you get from console.log. I use it whenever I'm inspecting API responses or state arrays.
console.group for Organized Output
console.group('User Authentication Flow');
console.log('Token found in localStorage');
console.log('Token expiry:', tokenExpiry);
console.log('Current time:', Date.now());
console.groupEnd();
Groups related logs under a collapsible header. Essential when multiple systems are logging simultaneously and the console becomes a wall of text. I wrap each major operation in a group during debugging sessions and collapse the ones I'm not currently interested in.
console.time for Quick Measurements
console.time('API call');
const data = await fetch('/api/dashboard');
console.timeEnd('API call');
// Output: API call: 342.7ms
Simpler and less error-prone than manually calculating Date.now() differences. The label links the start and end automatically. Use it when you suspect something is slow but don't need a full performance profile.
console.trace for Call Stack Visibility
function processOrder(order) {
console.trace('processOrder called');
// ...
}
Prints the full call stack at that point. Invaluable when a function is being called from multiple places and you need to know which call path is triggering unexpected behavior. Saved me hours when a React component was re-rendering excessively โ console.trace in the render function immediately showed which parent component's state change was causing it.
The Elements Panel โ Beyond Inspecting
Everyone knows you can right-click an element and inspect it. A few less obvious features hide in this panel.
Force Element State
Right-click any element in the Elements panel, select "Force state," and toggle :hover, :active, :focus, or :visited. The element stays in that state until you remove it. Essential for debugging hover styles, focus indicators, or active states that are impossible to inspect because moving the mouse to the DevTools dismisses them.
I spent an embarrassing amount of time trying to screenshot a tooltip by hovering, then frantically hitting the print screen key before the tooltip disappeared. Force state fixes that entirely.
Computed Styles Tab
When CSS isn't behaving as expected โ an element has the wrong padding, an unexpected font size, a color that doesn't match your stylesheet โ the Computed tab shows the final resolved values after all cascading, inheritance, and specificity battles. It also shows which specific CSS rule provided each value, with a link to jump to that rule in the Styles panel.
This is probably faster than reading through fifteen inherited and overridden rules in the Styles panel trying to figure out which one actually won. Go straight to Computed, find the property, click the source.
DOM Breakpoints
Right-click an element in the Elements panel and select "Break on" to set breakpoints that pause JavaScript execution when the element changes. Three options: subtree modifications (child elements added or removed), attribute modifications (class, style, data attributes change), or node removal.
Right-click element โ Break on โ Subtree modifications
This is โ I think โ the single best way to debug mysterious DOM changes. Something is adding a class to the body element that breaks the layout? Set an attribute modification breakpoint on the body. The debugger pauses at the exact line of JavaScript making the change. No more searching the entire codebase for classList.add.
Network Panel โ Simulating Reality
The Network panel shows every request your page makes. Everyone knows this. The advanced features are where it gets useful for debugging.
Throttling
The dropdown next to the "Disable cache" checkbox lets you simulate slow network conditions. "Slow 3G" throttles to roughly 400kbps download. "Fast 3G" gives about 1.5Mbps.
Your application probably feels fast on your gigabit office connection. Your users in rural areas on mobile data are having a different experience. Throttling reveals which resources are too large, which API calls take too long on slow connections, and where you need loading states that your fast connection never triggers.
I make it a habit to test critical flows on "Fast 3G" before shipping. Found issues every time. Images that take 8 seconds to load. Fonts that block text rendering for 3 seconds. API responses that timeout because the default timeout was 5 seconds and the slow connection adds enough latency to exceed it.
Custom Throttling Profiles
The preset throttling options are useful but generic. You can create custom profiles for specific scenarios.
Click the throttling dropdown, select "Add custom profile," and specify exact download speed, upload speed, and latency values. I have profiles for "Average US mobile" (10Mbps down, 3Mbps up, 50ms latency) and "Emerging market mobile" (2Mbps down, 500kbps up, 150ms latency) based on real-world data from analytics.
Request Blocking
Right-click any request in the Network panel and select "Block request URL" or "Block request domain." The request will fail on next page load. This simulates what happens when a CDN is down, an API is unreachable, or a third-party script fails to load.
I use this to test error handling for every external dependency. Block the analytics script โ does the page still work? Block the font CDN โ does the text render in the fallback font or does it disappear? Block the API โ does the user see a helpful error or a blank screen?
Most web applications silently depend on 5-10 external services. Blocking each one individually reveals which failures are handled gracefully and which ones break the page. The answer is usually "more things break than you'd expect."
Performance Panel โ Finding the Bottleneck
This is the panel that changed my debugging approach most dramatically, and the one I avoided for the longest time because it looked intimidating. The flame chart. The timings. The paint events. It's a lot of data.
Here's how to use it without getting overwhelmed.
Recording a Trace
Click the record button (or Ctrl+E), perform the interaction you want to analyze, click stop. That's it. DevTools captures everything that happened during that window โ JavaScript execution, layout calculations, paint operations, network requests.
Reading the Flame Chart
The flame chart shows function execution over time. Wide bars mean the function took a long time. Tall stacks mean deep call chains. Look for the widest bars first โ those are your bottlenecks.
Example flame chart reading:
Top level: 'onClick handler' โโโโโโโโโโโโโโโโโโโโโโโโ 180ms
โโโ 'validateForm' โโโโ 25ms
โโโ 'processPayment' โโโโโโโโโโโโโโ 120ms
โ โโโ 'encryptCard' โโโโโโโโ 80ms
โ โโโ 'API call' โโโโ 35ms
โโโ 'updateUI' โโโ 15ms
From this, it's immediately clear that encryptCard is the bottleneck. Without profiling, you might have guessed the API call was slow. The trace shows the truth: 80ms in encryption, 35ms in the network call. Optimize the encryption, or move it to a Web Worker so it doesn't block the main thread.
Layout Thrashing Detection
The Performance panel highlights forced reflow events in red. Layout thrashing happens when JavaScript reads a layout property (like offsetHeight), then writes a style, then reads again โ forcing the browser to recalculate layout between each read/write cycle.
// BAD: Layout thrashing โ forces reflow on every iteration
items.forEach(item => {
const height = item.offsetHeight; // Read (forces layout)
item.style.height = height * 1.2 + 'px'; // Write (invalidates layout)
});
// GOOD: Batch reads then batch writes
const heights = items.map(item => item.offsetHeight); // All reads
items.forEach((item, i) => {
item.style.height = heights[i] * 1.2 + 'px'; // All writes
});
The Performance trace makes layout thrashing visible as a series of "Layout" events in rapid succession, each one marked with a warning. Without the trace, you'd just notice "this loop is slow" and not understand why DOM manipulation of 100 elements takes 500ms instead of 5ms.
Memory Panel โ Hunting Leaks
Memory leaks in web applications are sneaky. The page works fine. Performance is good. Then after the user has been on the page for 20 minutes, everything slows to a crawl. Tab memory usage is at 2GB. The garbage collector is running constantly, pausing JavaScript execution.
Heap Snapshots
The Memory panel lets you take heap snapshots โ a picture of every object in memory at that moment. The technique for finding leaks is comparison.
Take snapshot 1. Perform the action you suspect is leaking (navigate to a page and back, open and close a modal, add and remove list items). Take snapshot 2. Switch to "Comparison" view, comparing snapshot 2 against snapshot 1.
Objects that were created between the snapshots and should have been garbage collected but weren't โ those are your leaks. Sort by "Size Delta" to find the biggest offenders.
Common leak sources I've found this way:
- Event listeners added in
useEffectwithout a cleanup return setIntervalcallbacks that outlive their component- Closures holding references to large DOM trees after component unmount
- WebSocket connections that aren't closed on navigation
Allocation Timelines
For leaks that happen gradually over time, the allocation timeline is more useful than snapshots. Click "Allocation instrumentation on timeline," perform the leaking action repeatedly, and stop recording. Blue bars on the timeline show allocations. Gray bars show allocations that were later garbage collected. Blue bars that stay blue โ those are retained allocations, probable leaks.
I found a particularly nasty leak this way. A search component was storing every API response in a ref that never got cleared. Each search query added 50-200KB of data that stayed in memory forever. After 100 searches, the page was using 20MB just for cached search results nobody would ever view again.
Sources Panel โ Debugging Without console.log
Conditional Breakpoints
Right-click a line number and select "Add conditional breakpoint." Enter a JavaScript expression. The breakpoint only triggers when the expression evaluates to true.
// Only break when processing admin users
// Condition: user.role === 'admin'
function processUser(user) {
updatePermissions(user); // โ conditional breakpoint here
sendNotification(user);
}
This replaces the pattern of adding if (user.role === 'admin') { debugger; } to your source code. No code changes. No risk of shipping the condition to production.
Logpoints
Right-click a line number and select "Add logpoint." Enter an expression to log. The expression is evaluated and printed to the console whenever execution reaches that line โ but execution doesn't pause. It's console.log without modifying your code.
// Logpoint expression: `Processing order ${order.id}, total: ${order.total}`
function processOrder(order) {
validateOrder(order); // โ logpoint here
chargePayment(order);
}
I've moved almost entirely from code-embedded console.log to logpoints. Same output. No cleanup needed. No chance of shipping debug logs. They disappear when you close DevTools.
Snippets
The Sources panel has a Snippets tab where you can save and run JavaScript code against any page. Unlike the console, snippets are persistent โ they survive page reloads and browser restarts.
I keep a collection of utility snippets that I run regularly:
// Snippet: "Highlight all images without alt text"
document.querySelectorAll('img:not([alt]), img[alt=""]').forEach(img => {
img.style.outline = '5px solid red';
img.style.outlineOffset = '2px';
});
console.log(
`Found ${document.querySelectorAll('img:not([alt]), img[alt=""]').length} images without alt text`
);
// Snippet: "Show all event listeners on the page"
const allElements = document.querySelectorAll('*');
const withListeners = [];
allElements.forEach(el => {
const listeners = getEventListeners(el);
if (Object.keys(listeners).length > 0) {
withListeners.push({ element: el, listeners });
}
});
console.table(withListeners.map(w => ({
element: w.element.tagName + (w.element.id ? '#' + w.element.id : ''),
events: Object.keys(w.listeners).join(', '),
count: Object.values(w.listeners).flat().length
})));
getEventListeners() is a DevTools-only function โ it doesn't exist in regular JavaScript. Snippets have access to DevTools APIs that normal page scripts don't.
Application Panel โ Storage Inspection
The Application panel shows everything your page stores locally: localStorage, sessionStorage, cookies, IndexedDB, Cache Storage, service workers.
Cookie Debugging
Click any cookie to see its value, expiration, path, domain, SameSite attribute, and whether it's HttpOnly or Secure. You can edit values directly or delete individual cookies. This is how I debug authentication issues โ check if the session cookie exists, verify its expiration hasn't passed, confirm the path matches the current page.
Service Worker Inspection
If your application uses a service worker (and PWAs or offline-capable apps do), the Application panel shows its status, lets you manually trigger update and unregister actions, and shows cached resources. The "Bypass for network" checkbox disables the service worker temporarily, which is essential when you're seeing stale content and aren't sure if the service worker's cache is serving outdated files.
Spent half a day once debugging why CSS changes weren't showing up in production. The service worker was caching the old stylesheet. Checking the Application panel would have revealed this in 30 seconds.
Lighthouse โ Automated Auditing
Lighthouse runs automated checks for performance, accessibility, best practices, and SEO. It's built into DevTools under its own tab.
The raw scores are less useful than the specific recommendations. A performance score of 72 tells you little. The recommendation "Reduce unused JavaScript โ 340KB of unused JS across 3 bundles" tells you exactly what to investigate.
A few caveats. Lighthouse runs against the current page in the current conditions. Results vary between runs โ network conditions, background processes, and browser extensions all affect scores. Run it 3-5 times and take the median, or use the "Navigation" mode with a clean browser profile.
Also, Lighthouse simulates a mid-tier mobile device and a slow 4G connection by default. Your desktop score will be very different from your Lighthouse score. This is intentional โ Lighthouse tests for the conditions most of your users experience, not the conditions you develop in.
Responsive Design Mode
Ctrl+Shift+M (or the device icon in the DevTools toolbar) toggles responsive design mode. The viewport shrinks to a mobile size, and you can select specific device presets or enter custom dimensions.
The feature most people miss: you can simulate device-specific characteristics beyond screen size. The device dropdown includes options for device pixel ratio, user agent string, and touch event simulation. Testing with "Responsive" and a custom width doesn't simulate touch events โ selecting a specific device like "iPhone 14 Pro" does.
I test every feature in at least three viewport widths: 375px (mobile), 768px (tablet), and 1440px (desktop). Responsive design mode makes switching between them instant. The alternative โ grabbing the browser window edge and dragging โ is imprecise and doesn't simulate touch events.
The Features I Use Daily
After all of that, here's what I actually open DevTools for most often, ranked by frequency.
- Logpoints โ replaced console.log entirely for debugging
- Network throttling โ test on Fast 3G before every deployment
- Console.table โ for inspecting data structures
- Force element state โ for debugging hover/focus styles
- Conditional breakpoints โ for debugging specific scenarios in loops
- Performance traces โ when something feels slow and I need to know why
- Heap snapshots โ when memory usage climbs unexpectedly
- Request blocking โ for testing failure handling of external dependencies
The rest I use occasionally but keep in my back pocket for when I need them. DevTools has an incredible depth of functionality, and most of it is discoverable just by right-clicking things and reading the context menu. Half the features I've described, I found by accident while right-clicking in a panel and noticing an option I hadn't seen before. That's a good habit to develop โ right-click everything in DevTools. There's almost always more than what's visible on the surface.
Keep Reading
- Web Performance That Actually Matters โ Beyond Lighthouse Scores โ DevTools profiling is half the story; this covers what to actually fix once you find the bottleneck.
- Frontend Testing โ What to Actually Test and What's a Waste of Time โ The debugging workflow pairs well with a solid testing strategy that catches issues before they reach production.
Written by
Anurag Sinha
Full-stack developer specializing in React, Next.js, cloud infrastructure, and AI. Writing about web development, DevOps, and the tools I actually use in production.
Stay Updated
New articles and tutorials sent to your inbox. No spam, no fluff, unsubscribe whenever.
I send one email per week, max. Usually less.
Comments
Loading comments...
Related Articles

Web Performance That Actually Matters โ Beyond Lighthouse Scores
What actually moves the needle on real user experience: fixing LCP, CLS, and INP with changes that users notice, not just scores that improve.

WebAssembly Demystified โ It's Not Just 'Fast JavaScript'
What WebAssembly actually is under the hood, why calling it fast JavaScript misses the point, and the Rust-to-WASM pipeline I use in real projects.

HTTP/3 and QUIC โ Why HTTP/2 Wasn't the Final Answer
The protocol running a third of the web that most developers haven't thought about. Connection migration, 0-RTT handshakes, and why switching from TCP to UDP was the only way forward.