Your Theme Has 340 Settings and Merchants Use Eleven of Them
Every setting is a conditional in Liquid, a branch in CSS and a support question. I counted what a real theme actually used, and the answer changed how I…
I inherited a theme with 340 entries in settings_schema.json. Colour pickers for fourteen distinct elements, six heading font choices, toggles for animations nobody enabled, and three separate ways to configure the same announcement bar.
Out of curiosity I pulled settings_data.json from every store running it and counted how many settings differed from their default.
The median was eleven.
Why this is a performance problem, not just a design one
Settings feel free. They are a JSON entry and a Liquid conditional, and neither costs much on its own. In aggregate they cost three ways.
Every setting is a branch at render time. Liquid evaluates conditionals per request, and a product card wrapped in six nested if blocks is evaluated six times per card, fifty cards per collection page. Individually trivial, collectively visible in render;dur.
Every setting generates CSS. Themes emit a block of custom properties in the layout, one per configurable value:
<style>
:root {
--color-heading: {{ settings.color_heading }};
--color-body: {{ settings.color_body }};
{%- comment -%} ...another 90 lines of this {%- endcomment -%}
}
</style>
That was 6 KB of inline CSS in the head on this theme, uncacheable because it is inline, on every page. Most of it set values identical to the defaults already in the stylesheet.
Every setting is a code path nobody tested. With 340 boolean and enumerated settings the combinatorial space is not testable, so in practice you test the defaults and a handful of common variations. The rest is untested code shipped to production, which is where the "the theme broke when I turned on sticky header with transparent header on a collection template" class of bug comes from. I wrote about one of those in the theme that broke after installing an app.
Measuring your own
# how many settings the schema defines
jq '[.[] | .settings // [] | .[] | select(.type != "header" and .type != "paragraph")] | length' \
config/settings_schema.json
# how many the merchant actually changed from default
jq -r 'to_entries | length' config/settings_data.json
Then the comparison that matters, which is which of those differ from the schema default:
python3 - <<'EOF'
import json
schema = json.load(open("config/settings_schema.json"))
data = json.load(open("config/settings_data.json"))["current"]
defaults = {}
for group in schema:
for s in group.get("settings", []):
if "id" in s:
defaults[s["id"]] = s.get("default")
changed = [k for k, v in data.items() if k in defaults and v != defaults[k]]
print(f"defined: {len(defaults)} set: {len(data)} changed from default: {len(changed)}")
print("\n".join(sorted(changed)))
EOF
Run that across a few live stores using your theme and you will get a very short list. Mine came back with logo, three colours, two fonts, favicon, social links, currency format and cart type. Eleven.
What merchants actually change
Consistently, across every theme I have looked at:
Brand identity. Logo, favicon, one or two colours, a font pairing. This is the entire point of theme settings for most stores.
Cart behaviour. Drawer versus page versus popup. A real decision with real consequences.
Product page layout. Media position, gallery style, whether to show quantity. Two or three choices.
Social links and policies. Text fields that must exist.
What they do not touch: animation timing curves, border radius per component, four different shadow intensities, letter spacing overrides, seven grid gap options, and separate colour pickers for every state of every button.
The counterargument, which is real
Theme store themes need breadth. A theme sold to twenty thousand merchants across every vertical genuinely does need more configurability than one built for a single client, because the alternative is those merchants editing code, and merchants editing code is how you get support tickets you cannot reproduce.
There is also a commercial reality: settings sell. A comparison table with "50+ customisation options" converts better than one with twelve, whether or not anybody uses them.
I do not think that invalidates the argument. It means the cost should be paid deliberately, and it should be paid in the schema rather than in the render path.
How to have both
Render defaults as static CSS, overrides as inline. Only emit a custom property when the merchant has changed it:
<style>
:root {
{%- unless settings.color_heading == blank -%}
--color-heading: {{ settings.color_heading }};
{%- endunless -%}
}
</style>
Everything unchanged lives in the cached stylesheet. On the theme above this took the inline block from 6 KB to roughly 400 bytes on a typical store.
Group settings so the unused ones cost nothing. A conditional around a group is one evaluation instead of twelve:
{%- if settings.enable_advanced_animations -%}
{{ 'animations.css' | asset_url | stylesheet_tag }}
{%- endif -%}
The asset only loads if the feature is on, and the twelve settings inside it are never read otherwise.
Delete settings that have never been changed. You can measure this. If no store in your install base has moved a setting off its default in a year, it is not a feature, it is maintenance burden and untested code.
Stop adding a setting for every review comment. The reflex when someone asks for a different border radius is to add a setting. The better answer is usually to pick a good default, because a setting is permanent and a default can be changed.
The rule I follow now
A setting must earn its place by being changed by at least five percent of stores, or by being unavoidable for a specific vertical.
Everything else is a well intentioned default that would serve merchants better as a decision you made for them. Themes are opinionated products, and the ones people enjoy using are the ones where somebody had the confidence to choose.
Eleven settings out of 340 is not a sign that merchants are unadventurous. It is a sign that they wanted a good looking store with their logo on it, which is what they came for, and every option beyond that was work for me and noise for them.
Related reading: Fast Is a Feature, and a Feature Needs an Owner explores a connected angle.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud