When a store is slow, the conversation jumps to headless quickly, and it should not. In almost every audit I have run, the platform was not the problem — twelve apps, a theme built to sell to a thousand different merchants, and a hero image nobody had looked at since launch were the problem.
That matters because the two paths are not comparable. Cleaning up a theme is days of unglamorous work with a known outcome. Rebuilding headless is months, and it only pays off under a specific set of conditions. Doing the cheap version first is how you find out which situation you are actually in.
Here is the work, in the order that removes the most milliseconds per hour spent.
What is actually slow?#
Find out before touching anything, because the intuitive answer is wrong often enough to waste a week.
Open the product page — not the homepage — on a throttled mobile profile and look at the waterfall. Then check the field data, because a synthetic run on your laptop is a best case and the store is being judged on real phones. If those two disagree, the field number is the one that counts.
| Cause | Typical share | Fixable in a theme |
|---|---|---|
| Third-party app scripts | 30–50% | Yes |
| Unoptimised images | 20–35% | Yes |
| Theme JavaScript and CSS | 10–25% | Yes |
| Fonts | 5–15% | Yes |
| Shopify itself | 5–10% | No, and it is rarely the issue |
Note the bottom row. Shopify's own response time is fast and consistent, and the share of a slow page it accounts for is small. Every other row is yours.
Measure the product page, not the homepage#
The homepage gets the attention and the product page carries the revenue. It is also usually heavier, because it loads the reviews app, the upsell app and the size-guide app that the homepage does not.
Get a baseline you can point at later#
Record the numbers before you start — field LCP at the 75th percentile, total page weight, request count, and the count of distinct third-party origins. Without them you cannot demonstrate the improvement, and demonstrating it is what buys you time to do the rest.
Why are apps the biggest problem?#
Because each one is a script from another origin that nobody owns, and stores accumulate them faster than they remove them.
A typical store installs an app for reviews, one for upsells, one for a size chart, one for loyalty, one for a popup, one for analytics the marketing agency wanted, and one that was trialled two years ago and never uninstalled. Each adds a connection setup, a download and main-thread execution — and the last one adds all of that for a feature nobody uses.
Uninstalling is not enough#
This catches almost everyone. Removing an app from the admin frequently leaves its script tags and Liquid snippets behind in the theme, so a store can be paying for apps it stopped using a year ago. Searching the theme for the vendor's domain is the only reliable check.
Census the origins#
The fastest way to see the true situation is to list every non-Shopify origin the page requests and what each one costs in requests, bytes and time. The technique is the same as auditing third parties on any site, and on a store the list is usually longer than the owner expects.
Defer everything that is not needed to render#
Reviews below the fold, chat widgets, loyalty badges and popups do not need to load during first paint. Deferring them until idle or until the section approaches the viewport routinely removes several hundred milliseconds without removing a feature.
Replace the heaviest app with theme code#
A size chart is a table. A recently-viewed carousel is a few lines of local storage. Some apps are shipping a few hundred kilobytes for something the theme could do in twenty lines, and swapping those out is the highest-value change on many stores.
What do you do about images?#
Serve them at the size they display, in a modern format, and stop the CDN doing it at full resolution.
Shopify's image CDN will resize and convert for you, and the theme has to ask. A product image referenced without a size parameter is served at whatever the merchant uploaded, which is frequently a 3000px file in a 600px slot — the single largest source of wasted bytes on almost any site.
{%- comment -%} Ask the CDN for widths, and describe the layout. {%- endcomment -%}
<img
src="{{ product.featured_image | image_url: width: 800 }}"
srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
{{ product.featured_image | image_url: width: 800 }} 800w,
{{ product.featured_image | image_url: width: 1200 }} 1200w"
sizes="(max-width: 640px) 100vw, 50vw"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy" decoding="async"> The sizes attribute is the one that gets skipped#
Without it the browser assumes the image fills the viewport and picks the largest candidate, so a srcset alone often makes a collection grid heavier rather than lighter. Describing the layout is the actual work and it is why a snippet worth reusing is worth writing once.
Never lazy-load the first product image#
It is the LCP element on a product page. Marking it loading="lazy" tells the browser to wait until it confirms the image is in view, which delays the exact thing being measured. Eager, with fetchpriority="high", is correct for the first one and lazy for everything after.
Set width and height on everything#
Shopify exposes the dimensions on the image object, so there is no excuse for an unreserved box. Missing dimensions are the most common cause of layout shift on a product page, and a shifting page produces mis-taps that cost orders directly.
Cap what merchandisers can upload#
A 6MB phone photograph dropped into a collection banner undoes the whole exercise silently. A stated limit and a resize step in whatever process adds images prevents the problem where it starts.
How much theme code can you remove?#
On a commercial theme, a surprising amount — it was built to support features you are not using.
A premium theme ships slideshows, mega-menus, quick-view modals, currency switchers, countdown timers and a dozen section types, because it has to sell to merchants who want all of them. Your store uses perhaps a third. The rest is CSS and JavaScript downloaded and parsed on every page for nothing.
Find the unused CSS and JavaScript#
The Coverage panel reports how much of each file went unexecuted on a given page. A theme showing 70% unused JavaScript on the product page is normal and is a large, safe saving once you know which parts are dead.
Delete sections you will never enable#
Removing a section file removes its CSS and JS along with it. This is safe when the section is not used in any template, and the theme editor tells you which are. Keep the deletions in version control so they survive a theme update.
Split the CSS by template#
Product-page styles do not need to load on the collection page. Most themes ship one large stylesheet because it is simpler; splitting the largest chunks by template is a genuine win on a heavy theme, and it is the same reasoning as bundle splitting anywhere else.
Watch for jQuery#
Older themes and older apps still pull it in, sometimes twice at different versions. It is worth checking, because removing a duplicate copy is free and finding one usually means finding other things too.
What about fonts?#
Two families, two weights, preloaded and swapped — and Shopify makes part of this easy.
Fonts block text from rendering and a store using four weights of two custom families is downloading several hundred kilobytes before a word appears. The fix is boring and effective: fewer weights, font-display: swap, and a preload for the one used above the fold.
Use Shopify's font_face filter#
For fonts from Shopify's library, font_face emits a correct declaration and serves from Shopify's CDN, which removes a third-party origin. Where the brand allows it, this is the simplest option available.
Self-host anything else#
A custom font loaded from a third-party font service is a DNS lookup, a handshake and a stylesheet before the font file is even requested. Hosting the file yourself removes all of that, and it is the same change worth making on any site.
Subset if the family is large#
A font covering scripts your store does not sell in is downloading glyphs nobody will see. Subsetting to the Latin range typically halves the file, and for a display face used only in headings the saving is larger still.
Which Shopify-specific things actually matter?#
A few, and they are not the ones people talk about.
Use sections everywhere, but not endlessly#
Online Store 2.0 sections are good for merchandiser control and each one is more Liquid to render and more CSS to ship. A page with twenty sections where five would do costs both server render time and page weight.
Keep Liquid loops small#
Rendering a loop over every product in a large collection, or nesting metafield lookups inside a loop, shows up as server response time before anything reaches the browser. Paginating and limiting is the fix, and it is easy to do accidentally when a collection grows.
Metafields are cheap to read and easy to overuse#
A handful per product is fine. Dozens, each fetched individually inside a template, is a measurable cost — worth structuring deliberately rather than adding one at a time until it hurts.
The theme check tool exists#
Shopify ships a linter for themes that catches performance anti-patterns along with correctness issues. Running it is minutes and it finds things a manual review does not.
Does the Shopify speed score matter?#
It is a rough directional signal and it is not the number to optimise against.
The score in the admin is a lab measurement of a sample of pages, and it moves for reasons that have nothing to do with your changes. It is useful for noticing a large regression and misleading as a target — the same distinction as any lab number versus field data.
Judge on field data#
The Core Web Vitals assessment for your store is what search ranks on and what reflects real customers. Chasing the admin score while field LCP stays flat is a common and unproductive pattern.
Comparing to other stores is not useful#
The score is affected by the sample of pages, the traffic mix and the apps installed. Two stores with the same score can have very different real experiences, which makes cross-store comparison close to meaningless.
In what order should you do this?#
Highest saving per hour first, which puts the unglamorous work at the top.
- Remove dead apps and their leftover script tags — often the single largest win, and it is deletion rather than development.
- Defer the apps that remain until idle or viewport, so nothing non-essential runs during first paint.
- Fix the images — CDN width parameters, a real
sizesattribute, eager first image, dimensions on everything. - Cut unused theme CSS and JavaScript, guided by the Coverage panel rather than by guessing.
- Sort out the fonts — fewer weights, self-hosted or Shopify-served, preloaded and swapped.
The first two are usually most of the improvement and require no design decisions, which makes them the easiest to get agreed. Do them alone first and measure, so the effect is attributable.
Ship in small batches on a duplicated theme#
Duplicate the live theme, change one category of thing, preview it, publish. Batching five changes into one release means a regression is a bisect rather than a revert — and on a store, a regression is lost orders while you work it out.
What results should you expect?#
On a neglected store, roughly a halving of page weight and a second or more off mobile LCP.
A representative case: 4.1MB product page, 2.9 seconds field LCP at the 75th percentile, eleven third-party origins. Removing three dead apps and their orphaned script tags, deferring four more, fixing image widths and cutting unused theme code took it to 1.3MB and 1.6 seconds, over about four days. No rebuild, no design change, no app the merchant was actually using.
The conversion effect is real and smaller than the headlines#
Going from three seconds to 1.6 on mobile produces a measurable lift, and it is a few percent rather than the doubling that case studies imply. Speed is a floor — it stops you losing people who never see the page, and it does not by itself persuade anybody to buy. The rest of the funnel is where the larger numbers live.
It will drift back#
Apps get installed, images get uploaded, sections get added. Without a check, a store returns to roughly where it started within a year — which is the argument for a budget rather than a cleanup.
How do you keep it fast?#
A stated limit and somebody who owns it, because this category always regrows.
One person approves app installs#
Not to block them, but to ask what the last one cost and whether the new one has a lighter alternative. Most stores have never had this conversation and it takes ten minutes.
Recheck quarterly#
Fifteen minutes: page weight, request count, distinct origins, field LCP. Compared against the baseline you recorded, it either confirms things are stable or catches the drift while it is still small.
Write down what was removed and why#
Otherwise somebody reinstalls the app you deleted, because nothing recorded that it cost 400KB for a feature nobody used. A short note in the theme repository is enough.
What does it cost?#
Three to five days for a full pass on a neglected store, and an afternoon a quarter afterwards.
The audit is a day. The app cleanup and deferral is a day or two, mostly spent testing that nothing broke. Images and theme code are another day or two. None of it is difficult and most of it is deletion, which is why it is a fraction of the cost of a rebuild for a large share of the benefit.
The honest counterweight: this work has a ceiling. A commercial theme carrying two years of merchandising decisions will not reach the numbers a purpose-built storefront can, and there is a point where further cleanup returns very little. If a store is already lean and mobile performance is still costing measurable revenue, that is the genuine case for a rebuild — but you can only know you are there by doing the cheap work first, and most stores never do.
Almost every slow Shopify store is slow for reasons that have nothing to do with Shopify. Four days of deletion usually beats a quarter of rebuilding.
Conclusion#
Start by measuring the product page on a throttled mobile profile and record a baseline — field LCP at the 75th percentile, page weight, request count, distinct third-party origins. Shopify itself accounts for a small share of a slow page; everything else on the list is yours to fix.
Apps are the biggest item and the cheapest to address, because most of the work is deletion. Uninstalling in the admin often leaves script tags behind, so search the theme for the vendor domain — stores routinely carry apps they stopped paying attention to a year ago. Defer everything that is not needed for first paint, and replace the heaviest ones with theme code where the feature is a table or twenty lines of JavaScript.
Then images: ask the Shopify CDN for real widths, write a genuine sizes attribute, set width and height from the image object, and keep the first product image eager while lazy-loading the rest. A srcset without sizes makes a collection grid heavier, not lighter.
Cut unused theme code using the Coverage panel rather than intuition — 70% unused JavaScript on a commercial theme is normal — and reduce the fonts to two families and two weights, self-hosted or served by Shopify, preloaded and swapped.
Expect roughly half the page weight and a second or more off mobile LCP on a neglected store, from three to five days of work. Then keep it there with one person approving app installs and a fifteen-minute check each quarter, or it comes back within a year. If you want this run properly on your store, it is one of the things I do.