I took raxxo.shop from a 62 Lighthouse mobile score to 98 without leaving the Liquid theme, and this is the exact punch list.
The biggest wins were image format, font loading strategy, and cutting app scripts, not clever tricks.
Shopify sections let you lazy-load whole page sections below the fold, which almost no theme uses by default.
Core Web Vitals on Shopify is a budget problem, not a skill problem. Set the budget, cut until you hit it.
raxxo.shop runs on a stock Shopify theme I wrote from scratch. 87 custom sections, 114 products, 186 blog posts. A month ago I ran a Lighthouse audit on the homepage and got a 62 on mobile. I was furious. The site felt fast. The site looked clean. The site was not fast. It was a 4-second LCP and a 0.21 CLS and every other metric you do not want. I spent one weekend going through every suggestion Lighthouse made, and I finished at 98. This is the Shopify theme performance checklist I wish someone had handed me, in the order the fixes mattered, with the numbers.
Shopify theme performance is a budget problem, not a skill problem. You have a finite amount of kilobytes before the user's first paint, a finite number of round trips, a finite number of JavaScript bytes the main thread can parse. When you spend those on apps, on fonts, on poorly sized images, the score drops and the user waits. When you spend them intentionally, a Shopify theme can be as fast as a hand-tuned static site. Mine is now.
The 62 Baseline
I ran the audit twice before touching anything. Mobile, slow 4G, cold cache. The numbers were:
| Metric | Before |
|---|---|
| Performance score | 62 |
| LCP | 4.1s |
| FCP | 2.3s |
| CLS | 0.21 |
| TBT | 680ms |
| Speed Index | 3.8s |
The biggest red flags from the Treemap report were telling. 780 KB of JavaScript before any app was even loaded. A 310 KB hero image served as a full-resolution JPEG. Three font families loaded synchronously, each with four weights. Two Shopify apps injecting their own scripts into the head. A cumulative layout shift caused by an image without explicit dimensions.
None of this was clever engineering. It was the default install of a reasonable theme pulling in reasonable defaults. Shopify does not ship a slow product, but the defaults assume you want every feature on. If you only use a few, the unused ones are still costing you.
The 9 Changes That Got Me to 98
Here is what I did, in the order I did it. Each change shipped independently and I re-measured after every one. The order matters because some changes depend on others, and because you learn faster when you can attribute a score jump to a single action.
1. One font family, preload only
I was loading Outfit in four weights and two backup sans-serif families for fallback. Three font families, 280 KB of WOFF2. I dropped to one family (Outfit), two weights (400 and 700), and preloaded the 400 weight only.
{%- liquid
assign font_variant = settings.type_body_font | font_modify: 'weight', '400'
-%}
Also added font-display: swap to every @font-face so text paints with a fallback while the web font loads, not after. LCP dropped from 4.1s to 3.2s on this change alone.
2. WebP and responsive images, everywhere
Shopify's image_url filter supports format conversion and responsive srcsets natively. I was not using either. Every product thumbnail was a full 1000x1000 JPEG regardless of the rendered size. Every hero image was a desktop-resolution file served to mobile.
{% assign img = product.featured_image %}

The hero image dropped from 310 KB to 42 KB. Thumbnails dropped 70% in aggregate. LCP dropped another 600ms. Also notice width and height attributes: those kill CLS for image-heavy pages because the browser reserves space before the image loads.
3. Lazy-load below-the-fold sections
This one almost no theme does by default. Shopify sections can be wrapped in an `` pattern for images, but the section itself still runs its Liquid and ships its HTML. For heavy sections (reviews, related products, galleries) you can wrap them in a custom element that renders a placeholder until the user scrolls into view.
`javascript
{%- render 'section-product-reviews' -%}
{%- render 'section-product-reviews' -%}
(() => {
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (!e.isIntersecting) return;
const tpl = e.target.querySelector('template');
if (tpl) e.target.innerHTML = tpl.innerHTML;
io.unobserve(e.target);
});
}, { rootMargin: '400px' });
document.querySelectorAll('[data-rx-lazy]').forEach(el => io.observe(el));
})();
`
The `` tag is inert until you move its contents into the DOM, so Shopify ships the section HTML to the client but the browser never parses it until the user scrolls within 400 pixels of the element. TBT (Total Blocking Time) dropped from 680ms to 290ms because the main thread had less work to do on first paint.
4. Cut two Shopify apps
This one hurts to write. I had a review app and a popup app installed. Together they injected 340 KB of JavaScript into the head and added three external render-blocking stylesheets. I evaluated what each did and whether I used it enough to justify the cost. I did not. I replaced the review app with native Shopify reviews (free, zero JS, renders in Liquid) and the popup app with a 40-line custom modal I wrote myself.
{%- for product in collection.products -%}
{%- if product.metafields.reviews.rating.value -%}
{%- for i in (1..5) -%}
{% if i <= product.metafields.reviews.rating.value.rating %}★{% else %}☆{% endif %}
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
Apps are the number one performance tax on Shopify stores I audit. Before installing any app, check its Lighthouse impact in a throwaway dev theme. If it adds more than 50 KB of JS to your critical path, you probably do not need it.
5. Defer every non-critical script
Every script tag in my theme now has either defer or is loaded at the end of the body. If a script does not need to run before the first paint, it should not block the first paint. I audited the head and found three scripts that blocked rendering for reasons nobody could explain. Two were from an analytics integration, one was from a checkout tweak I had written myself three years ago. All three moved to defer.
defer downloads the script in parallel with parsing but executes after DOMContentLoaded. async downloads and executes as soon as possible, which can be worse for scripts with dependencies. For 95% of use cases, defer is the right choice. TBT dropped another 120ms.
6. Inline critical CSS, async everything else
Shopify's default theme ships one giant theme.css file that blocks rendering. I split mine into two files: critical.css with the 6 KB needed for the header, hero, and above-the-fold product grid, and rest.css with everything else. Critical CSS is inlined into the head. The rest loads async via a trick that tricks the browser into treating a stylesheet as print-only on load, then swapping to all when loaded.
{{ 'critical.css' | asset_url | stylesheet_tag }}
FCP dropped from 2.3s to 1.1s. This is the single most impactful CSS change I have ever made on a Shopify theme.
7. Explicit dimensions on everything
CLS was 0.21, which is terrible. Anything above 0.1 is a fail. Going through the layout I found three culprits: product card images without width and height, a hero carousel that resized on font load, and an embedded video iframe that defaulted to zero height until the player initialized. Each got explicit dimensions.
...
...
CLS went from 0.21 to 0.02. The user experience of the site changed audibly: no more jumpy layout as things load in.
8. Preconnect and DNS-prefetch for third-party origins
The Shopify CDN is on a different origin (cdn.shopify.com), and if you use Google Fonts or any analytics, those are on other origins too. A `` hint tells the browser to open the TCP connection and do the TLS handshake before the first request to that origin, saving a few hundred milliseconds.
`plaintext
`
Small change, 80ms off LCP on slow networks. Worth it because the cost is zero.
9. Turn off the animation library I forgot about
I had installed a small animation library for one hero effect two years ago and forgotten to remove it when I redesigned. 18 KB of JavaScript loaded on every page for an effect that no longer existed. Grep is your friend. Grep every theme for `` tags and confirm each one still pays rent.
What Did Not Move the Score
Not every optimization was worth the time. A few I tried and abandoned:
Server-side HTML minification. Shopify already gzips responses, and the browser does not care about whitespace. Minifying my Liquid output saved a few kilobytes of transfer and had zero impact on score.
Splitting JavaScript into tiny chunks. HTTP/2 means many small files are free, but each file still has a parse cost. I tried splitting my custom JS into five modules and got worse TBT because each module had its own boot overhead. I consolidated to two files and the score went back up.
Switching theme engine. I spent an afternoon looking at Hydrogen and Dawn for comparison. Both are excellent. Neither was going to move my score from 98 to 99 in a way that justified migrating a 87-section theme. Stay on Liquid if Liquid is working. The engine is rarely the bottleneck.
Aggressive caching headers. Shopify's CDN sets these for you. You cannot meaningfully override them without a proxy. Skip this entirely.
Bottom Line
Shopify theme performance is boring work. No single change moves the score by 20 points. Nine boring changes add up to a 62 to 98 jump. Image format, font loading, app audit, deferred scripts, inlined critical CSS, explicit dimensions, preconnect, and one rogue animation library. Everything else was noise. If you want a one-weekend checklist for your own store, work through this list top to bottom, re-measure after each change, and stop when you hit 95. You will not miss the last three points and your users will not either.
Top comments (0)