Skip to content

A compendium of in-browser debugging techniques

I thought it would be fun to document in-browser techniques for debugging the way web pages look or behave. None of this is new — in fact, you would have likely been able to use most of these approaches a decade ago — but maybe you'll find here a trick or two.

Updated: Aug 14, 2021

Techniques

Console logging

Use console methods within your code. Some browsers also allow you to add logpoints to external code.

Watching

When something is in the global scope, you use a Live expression if available in the browser, or watch it manually with setInterval:

window.setInterval(() => console.log(document.activeElement), 1000);

Breakpoints

While logging is passive, breakpoints interrupt code execution.

Code breakpoints

Place breakpoints within your code with debugger statements or add them externally using functionality available in your browser's developer tools.

DOM breakpoints

You can instrument DOM elements on your own using MutationObserver.

Event breakpoints

Break on exception

Diagnostic CSS

This can be as simple as one declaration to see the border-boxes of all elements on the page:

*,
*::before,
*::after
{
outline: 1px solid red;
}

...or you can really go to town on the idea. Use CSS to point out bad HTML markup, or overlay a baseline grid:

.with-baseline-overlay {
position: relative;
}

.with-baseline-overlay::after {
--row-height: 16px;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
transparent 0 calc(var(--row-height) - 1px),
magenta calc(var(--row-height) - 1px) var(--row-height)
);
}

Jake Archibald points out that you can unhide a single element with visibility: visible to aid debugging paint issues.

Ahmad Shadeed has an entire book on debugging CSS that explores various techniques.

Tim Kadlec's CSS used to highlight potential performance issues

JS for visual bugs

Sometimes, when things don't look good you need for a little bit of scripting. Here are a few cases where I've reached for JavaScript to solve a CSS puzzle:

To find out what's causing horizontal scrollbars. Unexpected horizontal scrollbars are not always straightforward to diagnose. A short function can help you track down the elements that are poking through the viewport bounds:

Array.from(document.querySelectorAll('*')).filter(
el => el.getBoundingClientRect().right > window.innerWidth
);

To understand z-index puzzles. A lot of CSS properties cause elements to become stacking contexts, creating confusing stacking. The window.getComputedStyle() method returns the computed style for a DOM element that includes properties inherited from ancestors and the cascade, and it's perfect for rolling our own stacking context detector, at least as a stopgap solution until the feature makes it to browser developer tools.

Catch in the act

You can pause execution on the current page at any point by pressing the Pause button in the developer tools.

Some behaviors, however, depend on very particular interactions with the page. Elements that get added and removed from the DOM based on hover/focus events, or drag-and-drop gestures, are annoying to observe — simply moving to the dev tools changes the behavior. You can delay a pause from the console so that it coincides with you doing the thing you want to observe:

window.setTimeout(() => {
debugger;
}, 5000);

When the five seconds pass, you get a freeze-frame for you to observe and walk around. (Via Rodrigo Pombo)

Instrumentation

Instrumentation is a fancy word for wiring up objects and values so that you can observe their behavior.

Instrumenting a function:

const instrument = (fn, preferred_name) => {
const name = preferred_name || fn.name;
const instrumented = (...args) => {
console.log(`${name || 'anonymous'} called with:`, args);
const ret = fn(...args);
console.log(`${name || 'anonymous'} returned:`, ret);
return ret;
};
instrumented.name = name;
return instrumented;
};

Instrumenting an object with a Proxy:

const instrument = obj =>
new Proxy(obj, {
get(target, prop) {
if (typeof target[prop] === 'function') {
return new Proxy(target[prop], {
apply(fn, thisArg, args) {
console.log(`Calling ${prop} with:`, args);
let ret = Reflect.apply(fn, thisArg, args);
console.log(`Return value: ${ret}`);
return ret;
}
});
} else {
console.log(`Getting ${prop}:`, target[prop]);
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`Setting ${prop} to:`, value);
return Reflect.set(target, prop, value);
}
});

Global-scoping

It used to be the case that the most common way for scripts to coordinate was the global scope. A side-effect of such a setup is you're able to drill down into the application's deepest pockets by starting from objects accessible from the window object. Nowadays, with bundlers and modules being the norm (both of which encourage you to avoid the global scope), you have to be more intent.

Ambient information

When you want invisible information from code to be surfaced to the page without disrupting things too much.

One popular technique with the React crowd is changing the background of elements whenever they rerender to a random color:

const MyComponent = props => {
let style = {
background: `hsl(${Math.random() * 360},100%,50%)`
};
return <div style={style}>Hello world</div>;
};

Other things you might do is log the value to the document's title, via document.title = value.


With contributions by: Nathan Manceaux-Panot

https://alan.norbauer.com/articles/browser-debugging-tricks