The symptom
On three pages of this site, the hero (the headline and photograph at the top of the page) was missing 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.
It took us weeks to find. We shipped several fixes along the way. Each was a reasonable theory about image loading, and none of them was the cause.
The theories that were wrong
Everything about the symptom pointed at the image: a photograph arriving late, with no space reserved for it, pushing content down when it landed. So we reserved the space with width, height, and an aspect ratio. We preloaded the photograph from the head of the document. We filled the reserved space 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 we stopped guessing. We added a small, temporary script to the page, for phones only, that recorded where the hero actually was every hundred milliseconds for the first five seconds, then sent one line back to our server. The whole 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 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. A meaningful share of your visitors never sees any of those, and you won't notice, because they look fine on your computer.
If some visitors report a bug that you cannot reproduce, don't ship another theory. Put a measurement on the page for a day. It is smaller than the fix, and it shows you what is actually happening.
Get the LFW Weekly Recap
A newsletter sent every Friday with the week at LFW. One email a week, and no other blog posts.
We send one confirmation email first. Nothing else arrives until you click it.
Check your inbox
A confirmation link is on its way to . Click it to start the weekly recap.