Blog · Guides
Our Hero Image Was Disappearing on iPhones. An Ad Blocker Was Hiding It.
For weeks the hero on three of our own pages vanished for a second and a half on iPhones, on a cold load only. Nothing reproduced it in a lab.
A fifty-line field trace found it in three page loads: an ad blocker was hiding any element whose class started with ad-.
The symptom
On three pages of this site, the hero, the block with the headline and the photograph, was not there when the page first painted on an iPhone. The section below it sat at the top of the screen for about a second and a half, then the hero appeared above it. Reload the page and it was there instantly. Open it fresh in a private tab and it was gone again.
It only happened on iPhones. It happened in Safari and in every other iOS browser, which makes sense because Apple requires them all to use WebKit. It never happened on a desktop or an Android phone.
Sometimes even LFW has issues that do not give up easily. This one took weeks, and we shipped several fixes that did not fix it, because each one was a reasonable theory about image loading and none of them were the cause.
The theories that were wrong
Everything about the symptom pointed at the image. A hero photograph that arrives late, a box that is not reserved for it, content that shifts as the file lands. So we reserved the box with width, height, and an aspect ratio. We preloaded the photograph from the head of the document. We filled the reserved box with a blurred placeholder so it was never white. Each change made the page measurably better and none of them touched the bug.
We tried to reproduce it in Chromium with the network throttled to a slow phone connection. The hero was laid out from the first frame. We installed real WebKit on a server and held every image and font back by two and a half seconds. Same result. In the lab, the page was fine.
Measure it where it happens
At that point the honest move was to stop theorizing. We added a small script to the page, temporary and phone-only, that recorded where the hero actually was every hundred milliseconds for the first five seconds, and sent one line back to our server when it finished. The full thing was under fifty lines.
Show the code Hide the code JavaScript, 17 lines
// Sample the hero's position for five seconds, then send one beacon.
const hero = document.querySelector("header.pagehead");
const samples = [];
const t0 = performance.now();
const tick = setInterval(() => {
const r = hero.getBoundingClientRect();
samples.push([
Math.round(performance.now() - t0),
Math.round(r.top), Math.round(r.height),
getComputedStyle(hero).display,
document.styleSheets.length,
]);
if (samples.length >= 50) {
clearInterval(tick);
navigator.sendBeacon("/api/hero-trace.json", JSON.stringify(samples));
}
}, 100); Three page loads from the phone that showed the problem, and the answer was in the numbers.
What the trace said
At 12 milliseconds after the script started, the hero was 992 pixels tall, sitting right under the navigation bar, with two stylesheets on the page. At 114 milliseconds the hero was zero pixels tall at position zero, its computed display was none, and there were four stylesheets on the page. At 1,554 milliseconds it was back.
Nothing on the page adds stylesheets. No rule in our CSS can set that element to display none. No script on the page touches it. So something outside the page was injecting two stylesheets on a cold load, and one of them was hiding our hero.
The hero on those three pages had the classes ad-hero, ad-heroimg, and ad-sub. The prefix was short for agencies and advocacy, two of the markets those pages serve. An ad blocker does not know that.
Cosmetic filters hide elements whose class names start with ad-, and content blockers on iOS inject exactly those rules as stylesheets when a page first loads. The second pass, a second and a half later, restored the element, which is why it eventually appeared. On a reload the blocker's rules were already in place, so there was nothing to flash.
The fix
Rename three classes. The hero is now mk-hero, mk-heroimg, and mk-sub on the nine pages that used them, in all three languages, and there is a note at the top of the stylesheet so nobody names a class that way again. The fix was live twelve minutes after the trace came back.
Show the code Hide the code CSS, 3 lines
/* Class names: never start one with "ad", "ads", "advert", "sponsor",
"promo", "banner" or "popup". Ad blockers' cosmetic filters hide
elements by such prefixes. */ What to check on your own site
Open the source of any page and search for class= and id= values that begin with ad, ads, advert, sponsor, promo, banner, or popup. Common ones we see on association and nonprofit sites are ad-banner for an announcement bar, sponsor-logos for the people who paid for the conference, and promo for the membership drive. Every one of those is invisible to a meaningful share of your visitors, and you will never see it, because it looks fine on your computer.
And if you have a bug that only some visitors report and you cannot reproduce, resist the urge to ship another theory. Put a measurement on the page for a day. The measurement is smaller than the fix, and it tells the truth.