The Fix That Dropped the Score First
A mobile PageSpeed score that dropped before it went anywhere better - and the five separate things actually wrong underneath it.
A PageSpeed Insights audit rarely hands you one problem. It hands you a list, and most of that list turns out to be symptoms of the same two or three root causes wearing different names. This is what one mobile audit on this site's own stack actually turned up, including the fix that made things worse before it made them better.
Not anonymized. This is this site's own render-blocking chain, its own broken stylesheet path, and its own regression - kept here because the mistake in the middle of it is the kind that's easy to repeat.
The stack: GitHub Pages as the origin, Cloudflare in front of it for proxy and DNS, and an HTML5 template still leaning on jQuery.
The first thing the audit flagged was three render-blocking requests loading in serial instead of in parallel - a main stylesheet, Font Awesome, and a Google Fonts stylesheet - adding up to roughly 1.4 seconds of blocking time before anything could paint. That single finding turned out to be the entry point into four more issues that weren't visible until the first one got pulled apart.
@import to pull in Font
Awesome and Google Fonts - invisible to the browser's preload
scanner, turning three parallelizable requests into a serial
chain.
font-display set, letting the
browser hide text and icons entirely while they downloaded.
And underneath all of it: five separate analytics and tracking tools running at once, each one its own script, its own request, and its own share of main-thread contention.
Two of these fixes made things worse before they made things better. Both regressions are documented here alongside the fixes, because the wrong turn is the more useful part to remember.
The fix for the serial CSS chain was straightforward on paper -
replace the @import lines with direct
<link rel="stylesheet"> tags in the HTML head,
plus preconnect hints for the font domains, so the browser could
discover and fetch all three stylesheets in parallel from the
start. The first attempt moved those
<link> tags into a JS file instead, injecting
them via document.createElement - convenient to
write, and a real regression to ship. A script-injected
<link> isn't visible to the browser until the
script itself downloads, parses, and executes, which delayed
discovery later than the original @import chain in
some respects and caused a visible re-paint once styles landed
late. Anything affecting first paint belongs in raw HTML, not
JS-injected markup - JS injection is fine for things that don't
affect the initial render, like lang attributes, meta tags, and
analytics scripts.
Pulling the @import chain apart surfaced a second,
unrelated bug: the Font Awesome path resolved to
/css/fontawesome-all.min.css, but the file actually
lived at /assets/css/fontawesome-all.min.css. Since
@import failures don't always throw the same loud
console error a <link> tag does under strict
MIME checking, this had likely been silently broken - icons
invisible - for a while before this audit made it visible. The fix
was a corrected, root-relative path reference. The habit worth
keeping: confirm a broken asset reference via direct testing
rather than assuming a page that "looks fine" is actually loading
everything it references.
Cloudflare's Browser Cache TTL dashboard setting was overriding
whatever the origin sent, at a flat 4 hours - meaning near-total
re-download on any repeat visit within half a day. The first
attempt switched that setting to "Respect Existing Headers"
without first setting up rules to inject a better
Cache-Control value, which just let Cloudflare pass
through GitHub Pages' own default of max-age=600 -
ten minutes, worse than the original override. The actual fix was
two Cloudflare Transform Rules matching by URI path ending in
.css/.js (a year, immutable, since those
are versioned via ?v= query strings) and
.jpg/.png/.webp/.woff2
(30 days), combined with "Respect Existing Headers" so the
Transform Rules' values actually take effect instead of either
extreme.
Missing font-display: swap meant the browser could
hide text and icons entirely while custom fonts downloaded. For
Google Fonts, the fix was appending
&display=swap to the stylesheet URL. For the
self-hosted, minified Font Awesome file, there's no single
declaration to edit - the fix was a targeted sed pass
injecting font-display:swap; into every
@font-face block in the minified CSS.
The consistent cross-domain request turned out to be a self-hosted Matomo analytics instance on a second domain, loading a tracking script and pixel - legitimate and intentional, just not obvious from the audit alone. The fix wasn't removing it, since it's a real, wanted analytics tool; it was adding a preconnect hint to warm the connection ahead of time, in raw HTML rather than JS-injected, for the same first-paint-adjacent reasons as decision 1, just with lower stakes since it doesn't affect visual render.
| Score | Notes |
|---|---|
| 71 | Starting point - Font Awesome silently broken the whole time |
| 51 |
After moving CSS links into JS-injected
header.js
(regression)
|
| 59 | Partial recovery |
| 70 | After fixing the Font Awesome path and confirming the render-blocking chain clean |
| 59-53 | Run-to-run lab variance, confirmed via repeated, unchanged audits |
| 15.0s LCP | One anomalous outlier run - page verified reachable and fast via direct fetch immediately after, not reproduced as a steady-state issue |
None of the individual fixes were large - a handful of
<link> tags, two Transform Rules, a corrected
path, a sed command. What took longer was resisting the
urge to trust a single composite score. A 10-15 point swing across
consecutive, unchanged audits looks alarming until three or four
repeat runs show the same underlying issue every time - at which
point the swing is measurement noise, not a regression, and the
individual metrics (LCP, FCP, TBT, CLS) are the more reliable signal
than the composite number.
Diagnosing render-blocking chains, cache configuration, and asset-loading regressions like these falls under Web & Digital work - the same discipline applied here to this site's own front end.
Because the first attempt at the fix - moving stylesheet links into a JS file instead of raw HTML - solved the discovery problem for the browser's preload scanner while creating a worse one: nothing about styling was visible until the script itself finished loading. Anything on the critical rendering path needs to stay in static HTML.
GitHub Pages doesn't expose origin-level server configuration - no
.htaccess or equivalent. If a site is proxied through
Cloudflare, caching has to be solved at the edge, using Transform
Rules or Cache Response Rules, not at the origin.
Not on its own. Lab-based audits have real run-to-run variance from server jitter and simulated throttling inconsistency, even with zero code changes. It's worth running an audit three or more times and checking whether the same underlying issue shows up consistently before treating one outlier reading as a real regression.
There's no fixed number, but each additional tracker is main-thread contention and network weight that compounds - even when no single tool is individually "the problem." Five separate tools running at once on one site is a reasonable trigger to start asking which ones are actually still needed.
A composite score rarely tells the whole story. A short conversation is usually enough to tell whether there's a quick fix or something deeper worth tracing.