Shopify speed optimization: what actually moves the needle
A slow store costs you twice: shoppers leave before the page settles, and Google uses page experience as a tiebreaker between otherwise similar results. The good news is that most Shopify speed problems come from a short list of causes, and you can fix nearly all of them in the theme.
This is the order I work through them in. The first sections usually deliver most of the gain.
Start with the right numbers
Shopify retired its old "speed score" in 2024. It was a lab test averaged across a few page types, and it was easy to game without helping real shoppers. The admin now has a web performance dashboard (Online Store → Themes) built on the three Core Web Vitals, measured from real visitors:
- LCP (Largest Contentful Paint): how long until the main content (usually the hero or product image) appears. Good is under 2.5 seconds.
- INP (Interaction to Next Paint): how quickly the page responds when someone taps or clicks. Good is under 200 milliseconds.
- CLS (Cumulative Layout Shift): how much the layout jumps around while loading. Good is under 0.1.
Two habits make these numbers useful. Look at mobile first, because that's where most shoppers are and where every problem is magnified. And trust field data (real visitors) over a one-off Lighthouse run on your fast laptop. Lighthouse is great for debugging, not for judging.
1. Fix the LCP image first
On most storefronts the LCP element is an image: the homepage hero, the first product photo, the collection banner. It's also where the biggest avoidable delay lives. Shopify's own performance team found that 59% of Shopify pages lazy-load their LCP image, which pushes it back by around three seconds.
Lazy loading is for images below the fold. The one image a shopper sees first should load eagerly, at high priority, in the right size:
{{ section.settings.image
| image_url: width: 2400
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400, 800, 1200, 1600, 2400',
sizes: '100vw' }}
What each part does:
image_url: width:sets the largest version you'll ever need; Shopify's CDN generates the smaller ones.widthsproduces asrcset, so a phone downloads a 400–800px file instead of the full 2400px one.sizestells the browser how wide the image will actually render. Without it, the browser assumes full viewport width and often downloads something far too big. For a two-column grid, something like(min-width: 750px) 50vw, 100vw.fetchpriority: 'high'moves the image to the front of the download queue.
Everything below the fold gets loading: 'lazy' instead. And avoid putting hero content in a CSS background-image: the browser can't discover it until the stylesheet has loaded, so it always starts late.
image_tag also supports preload: true, but use it sparingly. Shopify's performance team calls preload a "footgun": preloading the wrong thing steals bandwidth from what matters. fetchpriority is the safer default.
2. Stop the layout jumping
Layout shift feels broken even when the page is fast. The usual causes are:
- Images without dimensions.
image_tagwriteswidthandheightfor you, so the browser reserves the space. If you build<img>tags by hand, add them yourself, or give the container anaspect-ratio. - Late-arriving banners and app widgets: announcement bars, review stars, "back in stock" forms, cookie bars. Reserve their space with a fixed height, or place them where their arrival can't push content down.
- Web fonts swapping in. A fallback font with different metrics reflows every line when the real font arrives. Matching the fallback's size (
size-adjustand the metric overrides) removes the jump.
3. Audit your apps and third-party scripts
This is the most common problem on established stores. Every app that adds a storefront script competes with your own content for the network and the main thread. Some of them are render-blocking: the browser stops building the page until the script has downloaded and run.
Work through it in this order:
- List what actually loads. Open Chrome DevTools → Network on your product page and filter by JS. Note every domain that isn't yours or Shopify's.
- Remove leftovers. Uninstalling an app doesn't always remove code it added to your theme. Search
theme.liquidand your snippets for scripts from apps you no longer use. - Prefer app blocks and app embeds over code pasted into
theme.liquid. They're managed from the theme editor, they're removed cleanly with the app, and they can load their scripts only on the pages that use them. - Defer everything that isn't needed for the first paint. Add
defer(keeps execution order) orasync(for independent scripts like analytics) to every script tag you control. - Question each tag. If an app's value doesn't clearly beat its cost on your key pages, it's a candidate for removal.
4. Keep JavaScript off the critical path (and INP low)
INP measures how quickly the page responds to a tap. It suffers when the main thread is busy running JavaScript at the moment someone interacts. The fixes are about doing less work, later:
- Render with Liquid, not JavaScript. Content that Liquid can render on the server shouldn't be assembled in the browser. It displays sooner, and search engines see all of it.
- Defer your own scripts, and load code for features people might never use only when they use them:
<script src="{{ 'product-form.js' | asset_url }}" defer></script>
// Load the size-guide code only when someone opens it.
button.addEventListener("click", async () => {
const { openSizeGuide } = await import(window.sizeGuideUrl);
openSizeGuide();
});
- Skip the heavyweight framework for things CSS and a few lines of JavaScript can do: accordions, tabs, sliders, sticky headers.
- Animate with
transformandopacityonly. Animating layout properties such asheight,topormarginforces the browser to recalculate the layout on every frame.
5. Load fonts efficiently
Fonts are easy to overdo: every weight and style is another file.
- Use a system font stack where the brand allows it. It costs nothing to load.
- Otherwise use fonts from Shopify's font library or self-host them on Shopify's CDN rather than a third-party font service, and let the text show immediately in a fallback while the font loads:
{{ settings.type_body_font | font_face: font_display: 'swap' }}
- Load only the weights you actually use. Two is usually enough; each extra one is another download.
6. Keep Liquid lean
Slow Liquid shows up as a slow server response before anything reaches the browser, and no front-end optimisation can make up for it. Watch for:
- Nested loops, especially looping over products and then over each product's variants or metafields. That's where render time grows fast.
- Over-fetching. Don't output data for every variant if the page only needs the selected one to start with.
- Repeated work inside loops. Filter a collection before looping over it, and move
assignstatements that don't change out of the loop.
A quick checklist
- Checked the web performance dashboard on mobile, not just desktop
- LCP image is
loading: 'eager'withfetchpriority: 'high' - Every image uses
image_tagwithwidthsand a realisticsizes - Below-the-fold images are lazy-loaded
- No hero content in CSS
background-image - Space reserved for banners, app widgets and fonts, so nothing jumps
- Leftover code from uninstalled apps removed
- App features added through app blocks and embeds where possible
- Every non-critical script loads with
deferorasync - No loops nested three deep in the templates that matter
Speed work compounds. Each fix is small, but together they're the difference between a store that feels considered and one that makes shoppers wait. If you'd like a second pair of eyes on yours, get in touch.