▷
I picked up DevTools the way most people do — Elements panel, Network tab, and then whatever I stumbled into while trying to fix something. Every so often I’d find a feature that would have saved me an afternoon a year earlier, so I started keeping a list.
This is that list. Chrome is what I use day to day, so that’s the reference, though Firefox and Safari have equivalents for a good number of these.
Keeping dropdowns open while you inspect them
The one that bothered me longest: you open a dropdown, a popover, a tooltip, an autocomplete — you click into DevTools to inspect it, the page loses focus, and the thing closes.
Open the Command Menu (Ctrl/Cmd + Shift + P), run Show Rendering, and tick Emulate a focused page. The page then behaves as if it still has focus while you poke at it, so anything that closes on blur stays open.
That covers most cases. For the rest — menus that close on outside click, or UI that only exists mid-animation — I pause the page instead. In the Console:
setTimeout(() => { debugger }, 3000)Run it, spend three seconds getting the page into the awkward state, and the debugger freezes everything exactly there. The DOM is still inspectable while paused.
Forcing element state instead of chasing it
Hover styles are hard to debug with a mouse, because the moment you move to the Styles pane the hover is gone.
In the Elements panel, select the element and click :hov in the Styles pane. You can force :hover, :active, :focus, :focus-within, :focus-visible and :visited, and the styles stay applied while you work.
Two neighbours I found useful while I was in there:
- The + button adds a new rule scoped to the selected element. Quicker than editing an existing rule and then remembering to undo it.
- Hovering a selector shows its specificity, which helps when a rule is losing and it isn’t obvious why.
Breaking on DOM changes rather than lines
Sometimes code removes your element, or adds a class, or clears an attribute, and it isn’t clear which code. I used to go hunting with breakpoints, which took a while.
Right-click the node in Elements → Break on → subtree modifications, attribute modifications, or node removal. The next time anything touches the node, the debugger stops with the responsible stack trace in front of you.
For events rather than DOM changes, the Console has helpers:
getEventListeners($0) // every listener on the selected elementmonitorEvents($0, 'focus') // log events as they fire$0 is whatever you last selected in Elements, $1 the one before, and so on. $$('a') is querySelectorAll that returns a real array. copy(someObject) puts it on your clipboard.
Ignoring other people’s stack traces
By default, a stack trace through React or your bundler’s runtime is mostly frames you can’t do much about. In Settings → Ignore List, you can enable the option to add known third-party scripts automatically, or right-click any frame and choose Add script to ignore list.
Stack traces then collapse down to your own code, and stepping through the debugger stops wandering into framework internals. This one changed my debugging more than anything else on the list, and I only found it by accident.
Logging without editing the file
Right-click a line number in Sources and add a logpoint. It logs an expression every time that line runs, without touching the source, without a rebuild, and without a stray console.log sneaking into a commit.
Conditional breakpoints live in the same menu, which helps a lot when the bug only shows up on the 400th item.
In the Console, the eye icon adds a live expression — a pinned expression that re-evaluates continuously.
Changing what the server sent you
Sources → Overrides lets you save local edits to any file the page loads, including files on a production server, and they persist across reloads until you turn them off. It’s a good way to reproduce a bug against a live site, patch the file locally, and confirm the fix before writing any real code.
The same mechanism can override response headers from the Network panel — right-click a request and edit them. Testing a CSP change, a caching header, or a CORS fix without a deploy is a nice thing to have.
A few other Network features I use often:
- The filter box takes a small query language:
status-code:404,-method:GET,larger-than:1M,mime-type:application/json. - Right-click → Block request URL, to see what the page does when an analytics script or a font never arrives. That’s a real state some of your users are in.
- Right-click → Copy as cURL or Copy as fetch, when you want to replay a request outside the browser.
The Rendering drawer
Same drawer as Emulate a focused page, and it answers a lot of questions I used to guess at:
- Paint flashing highlights what actually repaints. If the whole viewport flashes when one number changes, that’s a good lead.
- Layout shift regions shows exactly what moved, which is easier to act on than a CLS score.
- Emulate CSS media features covers
prefers-color-scheme,prefers-reduced-motion,forced-colors, and print. Much easier than digging through OS settings each time. - Emulate vision deficiencies is a quick sanity check on whether an interface still works without colour.
Two more nearby: the Animations panel lets you scrub a transition or slow it to ten percent speed, which makes animation timing far easier to reason about. And in Elements, the small badges next to a node (grid, flex, scroll, container) tell you what a node really is — the scroll badge is how I usually find the unexpected scroll container breaking position: sticky.
Working out why an interaction feels slow
For load performance, recording in the Performance panel and reading the insights it gives you covers most of it, and that ground is well covered elsewhere.
Interaction slowness took me longer to get a handle on. What helped was learning that Interaction to Next Paint isn’t one number — it splits into three parts with quite different fixes:
- Input delay — the browser was already busy when the user clicked. The fix is in whatever was running, not in your click handler.
- Processing duration — your event handler is slow. This is the part I used to assume was always the culprit.
- Presentation delay — the handler finished quickly but the next frame took a long time to paint. Usually layout or style recalculation.
The Performance panel shows this breakdown for interactions you record locally. Local recordings on your machine and your network are only a small slice of what real users get, so it’s worth measuring in the field as well. Google’s web-vitals library reports the subparts, and its attribution build uses the Long Animation Frames API to name the specific script and function responsible:
import { onINP } from 'web-vitals/attribution'
onINP((metric) => { navigator.sendBeacon('/analytics', JSON.stringify({ value: metric.value, inputDelay: metric.attribution.inputDelay, processingDuration: metric.attribution.processingDuration, presentationDelay: metric.attribution.presentationDelay, sourceURL: metric.attribution.longestScript.entry?.sourceURL, sourceFunctionName: metric.attribution.longestScript.entry?.sourceFunctionName, }))})Long Animation Frames is Chromium-only for now, so the function names won’t come back from every browser. The problems it points at are usually real everywhere.
Smaller things, no particular order
document.designMode = 'on'makes the whole page editable. Good for checking how a layout copes with longer copy.- Command Menu → Capture full size screenshot, or right-click a node → Capture node screenshot.
- The Computed pane has an arrow next to each value that jumps to the declaration that won.
Ctrl/Cmd+Shift+Fsearches every loaded file, which is how I track down a stray class name.- Application → Service Workers → Update on reload, for when a service worker is serving you yesterday’s build.
chrome://inspectgives you real DevTools against a page on a plugged-in Android phone, including port forwarding to your dev server. A lot more reliable than the device toolbar for mobile-only bugs.
None of this is new or clever — it’s all a right-click or a checkbox away. I just wish I’d found it sooner, which is why it’s written down.