E-Commerce Site Speed Fixes That Actually Move the Needle
Third-Party Scripts That Tank Your FCP
I remember using a heatmap tool once that quietly added 300ms to FCP without showing up in Lighthouse audits. Seriously. PageSpeed Insights showed green, but real Chrome users were seeing mega-jank. And here’s why: these widgets often hook into DOMContentLoaded
or even requestIdleCallback
, but they leak reflows through the backdoor via font injections or demand-janky CSS. The worst ones wrap themselves in setTimeout
chains that are invisible unless you’re watching timelines live with devtools throttled to Fast 3G.
Remove all third-party tag scripts and measure again. Yes, even your analytics. Run with query params to inject them only for admin. I’ve had multiple clients see full-second drops in FCP/FID just by toggling GTM-lite. GTM becomes a junk drawer. If you’re loading five A/B test helpers and that one affiliate pixel that no one in marketing remembers… congrats, you’re throttling your LCP with nostalgia.
Unreliable Hosting Auto-Scaling Isn’t Just About CPU
If you’re deploying to a VPS or a mid-tier cloud PaaS like Heroku or one of those Gen-Z hosting layers that promises elastic joy, be wary of the storage IO bottlenecks. Google will tank your rankings if Time To First Byte crosses 1 second, and you won’t even hit that with slow CPU. It’s disk IO that causes cold compiles for cached assets to fall apart. I once chased a 2s backend load time and it ended up being the shared volume mount latency on AWS EFS. Swapped to regional SSD on a custom cloud function and dropped the issue entirely. TTFB went from 1800ms to under 300ms.
Also: edge caching can backfire if not versioned properly. Cloudflare did me dirty by caching expired HTML due to a misconfigured cache-control
key on the homepage. I had changed the CTA inside a component. That component was pure client-rendered, but Cloudflare had already grabbed the static shell, and because of the ETag rules, it served a month-old variant until I manually busted it with a query string. Nightmare stuff.
Web Fonts and that Fake First Paint
Your fancy DTC brand font is probably giving Google’s automated bot vision the illusion of speed — because you’re drawing invisible text until layout flush — but your users are staring at blank H1s for 0.8s. That’s not performance. That’s props.
Add font-display: swap
. Force fallback immediately. Even better, preload the WOFF2 and serve critical text in system UI sans first (thankfully Googlebot has evolved to render some FOUT behavior correctly). Also test your CLS after injecting Google Fonts. Their own embed script ships delayed layout calls unless you self-host and subset properly. This one gets missed a lot in client audits. You think your CLS is zero because the homepage audit says so… but your product pages inject filters and buttons post-DOM. That counts. CLS isn’t a single score. It’s an ongoing slapfight.
Shopify Liquid Render Pipeline Quirks
Liquid’s server-side rendering isn’t always predictable. If you’re letting Shopify auto-inject their app blocks in between your custom sections, it can displace your preloaded resource hints. I had a client lose FCP stability because the Shopify Reviews widget delayed below-the-fold CSS parsing by reordering your <head>
manually.
Also — undocumented but real — their image CDN doesn’t respect max-age
on some resized variants unless you manually set format=auto
and width
. Otherwise, it regenerates thumbnails every time those filters change. Watch your waterfall.
“Minifying didn’t help. But swapping from {{ image.src | img_url: ‘300x’ }} to a hard-coded CDN path lock-stepped the cache back on track.”
Don’t trust variables alone. Check network headers directly in devtools: response vs request. Shopify lies through template abstraction layers.
Lite YouTube Embeds Break Consent Logic
The so-called lite embeds (like Paul Irish’s lite-youtube-embed
) are great until you realize they don’t fully respect GDPR-style user consent unless you customize them yourself. I ran into a case where my site was flagged for autoplay cookies — even though the iframe wasn’t loading the full player until click. Turns out, the thumbnail preview pinged a YouTube domain that already set an NID cookie. Took me two days to find it.
To avoid this failure:
- Use
data-src
and lazy swap triggers. - Host your own video thumbnails.
- Intercept iframe injection via a consent-managed dispatcher.
- Avoid loading anything from ytimg or googlevideo until opt-in confirmed.
- Never trust an embed package’s README promise — check your own browser’s storage tab.
- Double-check forms that hang beneath video areas, they may fail silently on scripts erroring out mid-hover.
This one’s extremely inconsistent across browsers, by the way. Chrome quietly loads NID from a service worker. Firefox blocks it at the fetch layer. Good luck debugging that with GTM bloated over everything.
How WebP Broke Add-To-Cart Buttons in Safari
I once replaced all product thumbnails with WebP. Worked like magic… until a customer said they couldn’t add to cart. Only in Safari. Only on the product detail pages. Turned out — brace for stupidity — the WebP was breaking a click
event cascade that used image dimensions via JS to calculate the trigger zone for the button(!).
Safari (at the time) didn’t reliably report dimensions on WebP-loaded <img>
tags unless they were fully decoded. So on slower connections, the button thought it was off-screen and refused to trigger the payload. I fixed it by preloading dimensions with HTML width/height, and also fall-backing to JPG for Safari at build time using UA detection in middleware. Yes, hacky. But this snag wiped out mobile conversion by probably 20% for a week.
Always test on real devices. Real slow devices. Not simulated Network Throttling in Chrome DevTools.
Troubleshooting TTFB With Cloudflare
Cloudflare’s ‘Cache Everything’ page rule sounds great on paper… until you realize it blindly caches login prompts, CSRF tokens, geo IP logic, and cart states. I had to dump our entire ruleset after accidentally caching a logged-in user’s admin header on the homepage. If someone had clicked Edit, that would’ve been game over.
The trick is to cache only anonymous GETs — and you can do this by fingerprinting cookies and using Edge Worker logic. Cloudflare Workers let you do this, but it adds cost and deployment overhead. That said, TTFB gains were real. From 800ms to sub-150ms consistently.
if (request.method === 'GET' && !request.headers.get('cookie')) {
// Keep going — safe to cache
} else {
// Bypass CF cache
}
Bug I hit? Even with caching disabled via headers, some HTML still got served from stale cache due to misaligned Edge configuration. Had to purge via API, not UI, because the UI purge ignored custom asset keys.
JS Framework Hydration Collisions With Shopify Plus Scripts
Ran into this weird behavior where SvelteKit and Shopify Plus’ checkout scripts were both initializing client-side chunks on the same div, causing shadow DOM collisions. The Shopify layer uses some archaic JS loader tied to their cart.js legacy system — which doesn’t always wait for full hydration. That meant users saw a flicker, and then a full cart reset.
Got past it by doing two things:
- Deferring third-party checkout init until Hydra loaded
- Wrapping all external scripts in try/catch when inside the checkout environment
The Shopify docs were zero help. This wasn’t even an intentionally exposed behavior. I figured it out by watching log diffs between the preview and production modes and spotting the extra shadow-root injection running from some script called tableau-loader.js(???). Don’t know why that’s in there. It’s not documented anywhere. Might’ve been introduced by an app. Either way, always isolate major component roots with unique IDs and monitor MutationObserver
at runtime to catch ghosts.