The Shopify admin shows a friendly "Update available" button on your theme. On a stock theme, you click it. On a shop with two years of custom Liquid in it, that button is a trap.
Mantraroma sells silver jewelry with real gemstones. The shop didn't need a rebuild — it needed to get faster, move to a current theme version, and let customers browse by gemstone or by jewelry type. All while keeping the custom features that were already there.
Here's what that actually involved.
Why a theme update stops being a click
A new theme version cannot simply overwrite individually changed files. Copy the old files over the new ones and you lose the theme developer's fixes. Take the new files wholesale and you lose your custom work.
So the upgrade to Symmetry 7.4.0 started with an inventory, not with an upload:
- Which snippets had actually been modified?
- Which settings belonged to the old theme version?
- Which app blocks and extra scripts were attached to which templates?
Only after separating theme default from app code from project-specific work could anything move. The new version went up as its own working copy, and customizations were transferred feature by feature — slower than copy-paste, but it preserves the upstream improvements you're upgrading for in the first place.
Then: home page, collection pages, product pages, cart and mobile navigation, checked against the old shop. Plus a performance measurement after every substantial change.
Filters that match how people actually shop
Jewelry browsing is preference-driven. Some visitors start with "I want a ring." Others start with "I want moonstone." A search box and rigid categories serve neither well.
So the shop filters on exactly two axes: gemstone and jewelry type.
For gemstones, a text list is useless — a stone is recognized visually before its name is read. The swatches are built from the existing variant images:
<!-- Theme settings: gemstone is registered as its own swatch option -->
swatch_option_name = "Edelstein"
swatch_method = "variant-images" // image comes from the variant
swatch_style = "icon_circle" // circular picker instead of a text list
enable_filter_swatches = true
<!-- Color values as a fallback when no variant image exists -->
swatch_value_list =
Amethyst:#9966cc Granat:#8c381c Labradorit:#657b83
Malachit:#0BDA51 Mondstein:#3aa8c1 Onyx:#353935
Rosenquarz:#f7cac9 Tigerauge:#e08d3c Türkis:#008888
<!-- Liquid renders the filter values available in each collection -->
{%- for value in filter.values -%}
<label class="filter-group__item{% if value.active %} …--active{% endif %}
{% if value.count == 0 %} …--disabled{% endif %}"
{%- if filter_group_is_swatch %} data-swatch="{{ value.label | downcase }}"
{%- if value.swatch and value.swatch.image %}
style="--swatch-background-image: url({{ value.swatch.image | image_url: width: 40 }})"
{%- elsif value.swatch and value.swatch.color %}
style="--swatch-background-color: rgb({{ value.swatch.color.rgb }})"
{%- endif %}
{% endif %}>
{%- endfor -%}
Jewelry type stayed text-based on purpose. "Ring", "necklace" and "earrings" are read faster as words than as tiny icons.
The unglamorous part that makes or breaks this: product data has to speak one language. "Moonstone", "moon stone", and a stone mentioned only in a product title are three different things to a filter. Most of the work was not presentation — it was a reliable relationship between filter value, product variant and image.
The performance series
Performance wasn't measured once at the end. Every stage got its own numbers, which turned out to be the useful part:
| Metric | Start | Lazy loading | Cookie banner | Minification |
|---|---|---|---|---|
| Pingdom grade | 71 | 69 | 69 | 72 |
| Page size | 6.1 MB | 2.0 MB | 1.6 MB | 2.1 MB |
| Load time | 8.11 s | 1.34 s | 1.04 s | 0.7 s |
| Requests | 168 | 167 | 140 | 134 |
| PageSpeed Desktop | 98 | 99 | 96 | 99 |
| PageSpeed Mobile | 56 | 51 | 86 | 86 |
| Mobile FCP | 7.0 s | 5.6 s | 2.3 s | 2.3 s |
| Mobile LCP | 24.8 s | 12.6 s | 3.5 s | 3.8 s |
Load time fell by roughly 91%. Page weight by about two thirds. Mobile PageSpeed went from 56 to 86 while desktop was already sitting at 98.
Now read the table sideways, because that's where it gets interesting.
Optimization is not linear. Lazy loading cut page weight and load time dramatically — and the synthetic Pingdom grade dropped from 71 to 69. Minification produced the fastest load time and the best desktop score while the measured page size went back up.
The biggest mobile win didn't come from minification. Mobile LCP dropped from 24.8 s to 3.5 s after the media load and third-party scripts were brought under control — before the final optimization stage ran at all.
Test timing, cache state, third parties and whichever hero image happened to load can all move individual numbers. The useful signal is never one green score; it's the pattern across load time, requests and Core Web Vitals together.
Filters and SEO have to be designed together
A good filter can generate an enormous number of URL states: rings with moonstone, necklaces with onyx, everything in a price band. Almost none of those are a page worth indexing.
The discipline:
- editorially maintained collection and landing pages stay the index targets
- filters help within those pages
- canonicals, Shopify's URL logic and a robots rule for variant parameters keep thin state URLs out of the index
- product and article pages carry structured data and unique metadata
Shopify fixes parts of sitemap and routing for you. The answer isn't to fight that with more scripts — it's to be rigorous on the layers you do control: clean product data, clear collections, internal links, and a theme that doesn't invent unnecessary URL variants.
Key Takeaways
- Treat a theme update on a customized shop as a migration. Inventory first, transfer feature by feature, never file-over-file.
- Build swatches from variant images you already have. A stored color value is a fallback, not the plan.
- A filter is only as good as your product data. Inconsistent naming turns one filter into three broken ones.
- Measure after every stage, not once at the end. The intermediate rows tell you which change actually did the work.
- One score is not a diagnosis. A synthetic grade can fall while the site genuinely gets faster.
Full technical write-up on hafenpixel.de. I've also written about replacing three Shopify apps with custom Liquid if you're in app-diet mode.

Top comments (0)