Why this exists

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 starting point

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.

What was actually wrong

  • The main stylesheet used CSS @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 Awesome was resolving to the wrong path and silently 404ing, likely for a long time before this audit surfaced it.
  • Every static asset was capped at a flat 4-hour browser cache TTL by a Cloudflare dashboard override, with no origin-level way to fix it since GitHub Pages doesn't expose server config.
  • Custom fonts had no font-display set, letting the browser hide text and icons entirely while they downloaded.
  • A consistent ~330-340ms cross-domain request to a second domain had no obvious explanation from the audit alone.

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.

The decision log

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.

Decision 1: Move the imports into HTML, not into JavaScript

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.

Decision 2: Verify the asset actually loads, don't assume it does

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.

Decision 3: Respecting existing headers isn't the same as setting good ones

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.

Decision 4: One line for hosted fonts, a different line for self-hosted ones

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.

Decision 5: Track down the mystery request before assuming it's a problem

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 timeline

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

What this actually took

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.

Frequently asked questions

Why did fixing performance make the score worse before it got better?

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.

Why can't cache headers be set directly on GitHub Pages?

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.

Is a single anomalous LCP reading something to worry about?

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.

How many analytics tools is too many?

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.

Not sure what's actually slowing your site down?

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.