Articles
Catching “The Ghost”: Debugging with MutationObserver and checkVisibility()
BY Alex Kondratiuk
•Tue, Oct 28, 2025

Some bugs leave a trail of breadcrumbs. Others — like the one we nicknamed “the ghost” — leave nothing at all, or so it seemed.
This ghost was a 50-millisecond white flicker appearing randomly on one of our clients’ production pages across their sites. It showed up after the initial render, left no clues in performance traces, and disappeared before any developer could confirm what they’d seen.
This is the story of how we used a new Web API — MutationObserver — and its partner in crime, checkVisibility(), to capture a seemingly invisible bug that haunted production for days and had us scratching our heads.
The Problem: A Phantom Flicker
It started like most front-end mysteries. Everything looked fine in local and staging environments, but in production, a brief flicker would flash across the screen about one second after page load.
At first, it seemed tied to ads — they rendered right before the flicker occurred. Disabling ad networks didn’t help. Blocking third-party scripts helped reduce the frequency, but with hundreds of scripts running across the site, that wasn’t a viable solution.
We suspected the Adobe Launch script but we weren’t able to reproduce the issue consistently. Even with throttled CPU and performance recordings, no errors or DOM reflows appeared in the trace. The browser’s Developer Tools were silent.
After a couple of days of chasing shadows, we decided that it was time to change our approach.
Discovering MutationObserver
That’s when we found MutationObserver, a browser API.
MutationObserver lets you listen to changes in the DOM asynchronously — every time an element is added, removed, or modified, the observer logs it as a “mutation record.” These records can be filtered by type (child additions, attribute changes, text updates, etc.), making it ideal for debugging visual inconsistencies that happen outside the normal JavaScript execution flow.
Here’s the essential setup:
// Select the element to observe
const targetNode = document.querySelector('h1');
// Configure what to observe
const config = { childList: true, subtree: true };
// Create the observer
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
console.log('Mutation detected:', mutation.addedNodes);
}
}
});
// Start observing
observer.observe(targetNode, config);
This code watches for childList changes within the subtree of the h1 element — meaning it logs anything that’s dynamically injected or removed from that section of the DOM.
By keeping the observation scope narrow, we avoid being flooded with mutations from unrelated scripts. It’s a good balance between signal and noise.
Adding Visibility Detection
To detect if something was visibly causing the flicker, we paired the observer with another new API: checkVisibility().
checkVisibility() answers a simple question: Is this element actually visible to the user right now?
It considers geometry, layout, and CSS properties (like opacity or visibility), returning true only when an element is perceptible on-screen.
For example:
const element = document.querySelector('h1');
if (!element.checkVisibility()) {
console.warn('Element temporarily hidden — potential flicker detected!');
}
By combining these two APIs, we could track both what changed and whether the user could see it.
In our setup, whenever the h1 tag’s visibility changed, we logged the corresponding mutation. That let us correlate DOM events with what users were actually seeing — the missing link in our earlier debugging attempts.
Capturing the Ghost in Action
Armed with our observer, we reloaded the production page multiple times until the flicker reappeared.
This time, we had evidence.
The observer logged two distinct mutations:
- A harmless
<span>element being added (likely part of a third-party tracker). - A dynamically injected script called
enableFlickerControl.
That name alone was suspicious enough. A quick search confirmed our hunch — it belonged to Parsley, an A/B testing framework integrated into our client’s analytics stack via Adobe Launch.
“Enable Flicker Control” was meant to prevent layout shifts during headline tests by briefly hiding elements until new content was ready. Ironically, the script itself caused the flicker it was trying to prevent.
Finding the Root Cause
Now that we knew what was being injected, we needed to know how.
Digging through the Adobe Launch configuration revealed that enableFlickerControl was injected through a launch rule tied to headline testing. Once we contacted the analytics team and disabled that rule, the flicker disappeared instantly.
What had taken a bunch of days of blind debugging to no avail was solved within hours of adding a MutationObserver. Relief all round!
Beyond Flickers: Broader Debugging Applications
It is worth stating that MutationObserver and checkVisibility() aren’t just for flickers. They’re useful for a wide range of debugging scenarios where the DOM changes without direct code execution signals, such as:
- Missing or disappearing elements: Catch when nodes are unexpectedly removed.
- Unintended third-party injections: Identify scripts modifying content after render.
- React hydration or race conditions: Detect when dynamic updates overwrite pre-rendered content.
- Performance regressions: Pinpoint elements repeatedly added and removed during reflows.
In all of these cases, the MutationObserver serves as a microscope for your DOM — revealing what’s happening between the frames that performance tools often miss.
Lessons Learned
- Traditional debugging tools don’t see everything.
- Network and performance traces show execution flow, but not structural DOM changes. MutationObserver fills that gap.
- Scope your observers tightly.
- Track only what’s relevant — watching the whole document can quickly flood your logs.
- Pair with visibility checks.
checkVisibility()turns raw DOM data into user-impact data. You’re not just seeing that something changed, but whether users noticed it.
- Intermittent bugs demand persistent observers.
- Even a flicker that happens once every ten loads can be captured if you’re patient and logging precisely.
Conclusion
Debugging “the ghost” reinforced a timeless lesson: complex systems often fail in subtle, invisible ways. MutationObserver thankfully gave us the visibility we needed — literally — to catch an invisible bug.
If your UI flickers, shifts, or ghosts out of existence without explanation, don’t just check the console or performance tab. Watch the DOM itself.
After discovering and using this tool to hunt down this elusive bug, I would wholeheartedly recommend adding MutationObserver to your debugging toolkit. While it requires writing a short, custom script to target the issue, this small investment pays off immensely - allowing you to isolate the root cause in hours rather than days.
If you are looking for technical support with your project, feel free to get in touch with us.