Fixing Mobile Page Speed Wreckage Before Your RPM Tanks
Cloudflare Cache Settings That Quietly Ruin Mobile Page Speed
If your mobile site feels jammed up like freeway traffic at 6 PM, take a breath and check your CDN policies—especially on Cloudflare. I once couldn’t figure out why my Time to First Byte was jumping from good to awful on mobile when desktop remained fine. Turns out, the “cache everything” page rule I hacked together back in 2020 was treating responsive HTML like static content. Mobile users were seeing stale desktop-cached HTML containers. ♂️
Clear the static HTML from the edge—or use Cache Keys to vary by User Agent, though beware that introduces serious scale chaos on high-traffic sites. Safer fix? Use Cloudflare’s Device Type in Page Rules and let mobile and desktop cache separately. You’ll lose some edge cache savings, but at least you’re not delivering 1440px-wide layouts to someone on a Moto G from 2017. Been there. Watched my bounce rate eat itself alive.
Under Caching → Configuration, if you have Polish or Mirage enabled, test lazy-loading behaviors indoors. On a Blackview Android. With throttling on. Mirage + large JS libraries = phantom layout shifts you’ll swear don’t exist until you’re running Chrome DevTools at 4x mobile emulation with CPU throttling.
AdSense Auto Ads Slowing Down the Fold
This one took me an embarrassing number of weeks to admit: Auto Ads on mobile are sneaky fast to enable… and sneaky slow to render. They don’t reliably show up in Chrome mobile Lighthouse. That script is dynamically injected after observable metrics fire, so you get A+ scores, but users still feel the molasses load.
One trick: manually place ads for above-the-fold if you’re chasing Core Web Vitals. Just <ins>
them in with enough spacing, then delay Auto Ads until the first interaction. Not strictly by the book, but Google doesn’t penalize you for script deferral that occurs after LCP.
Also—Mobile Ads Anchor Load Bug: if you’re using the anchor ad format, and you’re deferring adsbygoogle.js
with any async framework like Astro or React SSR… on some devices, the anchor ad will snap into place during scroll instead of initial load. I caught this on a friend’s Huawei using Edge Browser for Android. Couldn’t reproduce it on Samsung or Pixel. No doc anywhere mentions this quirk.
JavaScript Chunking That Actually Helps Mobile Load Time
I used to throw defer
on everything and shrug. Then I spent 3 hours on a layover trying to figure out why my homepage js size had ballooned 10x on mobile only. Turned out, Webpack was smart enough to lazy-load… until I added a dark-mode toggle that pulled in a full theme.js chunk based on prefers-color-scheme. Didn’t run unless toggled—but still blocked painting because dynamic imports live under main.js
umbrella by default.
Tips that actually moved the needle:
- Use Chrome Coverage tab to identify code never run on mobile—but loaded anyway. That was humbling.
- Split interaction scripts—tap handlers, modals, lightboxes—into
async type="module"
chunks triggered only after first scroll. - Lazy-import AdSense wrappers. Literally wrap it in a
requestIdleCallback
and watch TTI improve. - Bundle analytics and third-party pixel scripts together into one module and delay it via
document.hidden
or similar passive triggers. - Turn dead code into literal fragments. Don’t delete your AB test toggles, just offload them to CDN JSON fetch.
There’s no need for the whole IntersectionObserver
polyfill tree if you’re not supporting iOS below 12.3. That tree is 70KB of pure disrespect in 2024.
Font Loading: The FOUT That Made Me Rethink Self-Hosting
Okay, story time. I fought hard to self-host fonts to eke out control over preload timing. I wrangled font-display: swap
, custom @font-face
definitions, and preloaded both WOFF2 and WOFF. Still, on mobile Safari I noticed nasty flashes for less than 1s. Apparently, if you preload fonts but don’t also explicitly match your font token ordering, Safari sometimes skips to fallback for initial paint.
This is one of those platform bugs that nobody documents properly. Multiple threads on WebKit bugs mention it, but the workaround is to define font names in the exact load order AND make sure you use the exact case-sensitive naming used in CSS. “Poppins-Bold” is not the same as “Poppins Bold” per Safari font cache behavior.
Then there’s Lighthouse FID and CLS flailing over custom icon sets using base64 inlined webfonts. Those will bloat your initial CSS payload and interfere with text paint.
Viewport Meta Tags Screwing Up LCP Behavior
You want mobile speed? Don’t let your HTML fight the viewport. I once debugged a hero image delay that only affected iOS Safari—it hovered 400px below the fold and didn’t count toward LCP at all. Why? The meta viewport had user-scalable=no
, which in iOS triggered a different rendering path that deferred image prioritization in memory-constrained conditions.
<meta name="viewport" content="width=device-width, initial-scale=1">
That exact string works nicely across modern devices. You throw anything else in there, ESPECIALLY fixed widths (saw someone use 320 once), and your image LCP will break like a wet cookie in tea.
Real World CDN Behaviors That Don’t Show in Lab Tools
There’s this persistent issue where your Google PageSpeed scores look amazing… until you test on a throttled 3G Android stack. I watched a site load in 2.5 seconds in Lighthouse but took nearly 11 seconds real-world. The culprit? CDN compression varied by header. Real devices weren’t receiving Brotli because my device detection added a Vary: User-Agent
that invalidated cache-sharing on edge.
The edge case is this: unless you explicitly cache per header combo, CDNs like Cloudflare won’t share Brotli-compressed assets across different UAs. So unless mobile and desktop requests align exactly, you’ll lose all the benefit of compression policies silently. There’s no alert. No error. Just degraded experience and no hint unless you check the content-encoding
header manually on real devices.
“aha” moment: Fiddling with curl I saw this:
curl -H "User-Agent: mobile-test" -I https://yoursite.com/style.css
and the result lacked Content-Encoding: br
. Desktop UA? Brotli was there. Mobile? Nope.
Fixed it by setting a cache key override rule that excluded User-Agent entirely from CDN edge logic—but only for static assets. You definitely don’t want to do that on HTML unless you hate responsive layouts.
Third-Party JavaScript Holding Mobile RUM Hostage
Ever had your real user metrics tank during a campaign and couldn’t blame anything on your code? Check the false positive rate on real-user monitoring (RUM) when embedded third-party JS injects during slow network connects.
I was running CookieBot and Cloudflare Analytics on a client’s m.site subdomain. Network dropped to 2G mid-scroll and I watched the whole context window shift in Window.Metrics queue debug. Turns out, the way these scripts write into the DOM kicks off layout recalculations that overlap with interaction logging.
Not documented anywhere I could find, but if Memory pressure on WebKit hits during third-party doc.write() injection, layout is prioritized after interaction timing. That’s how I ended up with a bounce rate of 90% and no console errors.
Solution: buffer window.onload
hooks until document.visibilityState === 'visible'
AND navigator.connection.downlink > 1
. Otherwise, bail on firing RUM events until the browser isn’t panicking under throttling.
Lazy Loading Images Without Screwing CLS
This one has become buzzwordy, but still getting it wrong: if you lazyload images using native loading="lazy"
but don’t presize them with width
and height
attributes, CLS on mobile is gonna spike like it’s the Chicago Cold Storage Index.
I did this on a photo blog where images were fetched cloud-side and pre-cropped. Forgot to render dimensions server-side. Mobile loads would shove down the first paragraph TWO viewports deep once the browser figured out layout. Yeah,
Web Vitals hated it.
Your fix: inliners. Serve predictable dimensions at render time. Dynamic widths? Fine, just set a min-height using aspect ratio div wrappers. This isn’t 2012; CSS knows figure-ground by now.
And don’t trust Vue or React SSR image components without testing the hydrated state. I’ve seen Next.js Image behave nicely in dev but shift layout by 20+ px once loaded in production with edge caching enabled.