Page speed is half of my job at Renaissance Jewel. The store sells engagement rings, and a ring page there is heavy for reasons that are all fair.
First, look at what a ring page loads:
- Ella, the theme. It is a paid theme with a very long feature list.
- Apps for reviews, email capture, wishlists and shoppable video.
- Analytics.
- A 3D model of the ring, coloured to match the metal and the diamond on that page.
- Product photos. Jewelry sells on photos, so there are many.
Every item on this list is there because somebody wanted it, and most of them help sales. So I cannot delete things to make the page fast. Most of the work is deciding what loads first and what can wait.
Before changing anything: find what is heavy
Three free checks tell you where the weight is. They take ten minutes.
- Network tab, sorted by size. Open a product page in Chrome DevTools, go to Network, and sort by size. The top ten rows are your problem. On a jewelry store it is nearly always images first, then app scripts.
- Coverage. In DevTools, open the Coverage panel and reload. It shows how much of each CSS and JavaScript file was used on this page. A theme file that is 80% unused on the product page is normal for a big theme, and it tells you which features to switch off.
- The
<head>. View the page source and read the head from top to bottom. Every<script src>there withoutasyncordeferstops the page from drawing until it has downloaded.
Do this first. Without it you will spend a day on something that saves 20 milliseconds.
1. Images at the size the layout needs
This is the biggest win on almost every Shopify store, and the easiest.
Older theme code often asks for the original file:
<img src="{{ media | img_url: 'master' }}">
master means the file exactly as it was uploaded. A photographer’s export can be 4,000 pixels wide. A phone shows it at 400.
The fix is to ask Shopify’s image CDN for the widths you need, and let the browser pick:
<img
src="{{ media | image_url: width: 800 }}"
srcset="
{{ media | image_url: width: 400 }} 400w,
{{ media | image_url: width: 800 }} 800w,
{{ media | image_url: width: 1200 }} 1200w,
{{ media | image_url: width: 1600 }} 1600w"
sizes="(min-width: 990px) 50vw, 100vw"
width="{{ media.width }}"
height="{{ media.height }}"
loading="lazy"
alt="{{ media.alt | escape }}">
Three attributes do the work. srcset lists the sizes that exist. sizes tells the browser how wide the image will be on screen, so it can choose one. width and height let the browser keep the space free before the image arrives, so the page does not jump.
One exception: do not put loading="lazy" on the first product image. It is the main thing on the screen, and lazy loading makes it arrive late.
2. The 3D viewer loads only when opened
The 3D viewer is the heaviest single thing on a ring page, and many visitors never touch it.
A viewer like this is usually an <iframe>. An iframe in the HTML starts loading with the page, even if it is on the second slide of the gallery where nobody can see it yet.
The pattern to fix it is called a facade. The page shows a picture with a “View in 3D” button. The iframe is created only on click:
<button class="viewer-facade" data-src="{{ product.metafields.custom.model_source_embed }}">
<img src="{{ product.featured_media | image_url: width: 600 }}" alt="View this ring in 3D" loading="lazy">
<span>View in 3D</span>
</button>
<script>
document.querySelector('.viewer-facade')?.addEventListener('click', (event) => {
const button = event.currentTarget;
const frame = document.createElement('iframe');
frame.src = button.dataset.src;
frame.allow = 'fullscreen; xr-spatial-tracking';
frame.title = '3D view of this ring';
button.replaceWith(frame);
}, { once: true });
</script>
People who want to spin the ring wait a moment for it. Everyone else does not pay for it. The same pattern works for YouTube videos, maps and chat widgets.
3. App scripts wait
Many scripts load at the top of the page even when nothing at the top of the page uses them. A reviews widget sits far down the page. An email popup does not show in the first second.
For scripts that you added to the theme yourself, there are two levels of fix.
The small fix is defer. The script downloads in the background and runs after the page is parsed:
<script src="{{ 'slider.js' | asset_url }}" defer></script>
The bigger fix is to not load the script at all until the visitor does something:
function loadLater(src) {
let done = false;
const load = () => {
if (done) return;
done = true;
const script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
};
['scroll', 'pointerdown', 'keydown'].forEach((name) =>
window.addEventListener(name, load, { once: true, passive: true })
);
setTimeout(load, 5000); // safety net for visitors who only read
}
The first scroll, tap or key press loads it. A visitor who leaves in two seconds never downloads it.
Be honest about the limit here. Scripts that an app injects through Shopify’s app embeds are not yours to move. For those, your choices are the app’s own settings, or asking if the store still needs the app. Two apps that do the same job is a common find.
4. Turn off theme features nobody uses
A big paid theme ships with dozens of features: quick view, compare, recently viewed popups, “someone just bought this” notices, a currency switcher, and more. Each one that is switched on adds CSS and JavaScript to every page.
I went through the theme settings one by one and asked a simple question for each: does this store use it? What was not used got switched off.
This is boring work. But a switch in theme settings removes code from every page of the store at once, and it needs no code change that could break something.
What I did not do
I did not replace the theme. A rebuild on a lighter theme would give a faster base, but my brief is to make this store faster, not to build a new one.
I also did not treat apps as the enemy. Reviews and wishlists are there because they help sell rings. They stay, and where I can control it, they load later.