Skip to content
Ecom

Liquid vs JavaScript: Where Logic Should Live in Your Shopify Theme

The rule we use on every WitsCode Shopify build: if it can be Liquid, it must be Liquid. Why server-rendered logic beats client-side hydration for SEO, Core Web Vitals, and conversion, with the two...

By WitsCode9 min read

Every Shopify theme we audit has a see-saw problem. On one side sits Liquid, the server-side templating layer that runs inside Shopify's CDN-backed renderer and ships fully assembled HTML to the browser. On the other side sits JavaScript, downloaded, parsed, executed, and hydrated in the browser milliseconds or seconds after the page has already started painting. The see-saw is almost always tilted the wrong way. Product copy that should be static HTML is being fetched by a script. Spec tables that should be in the document from byte zero are being assembled by a React island after the main thread is free. Price displays that Liquid could render in three milliseconds are instead being calculated client-side after a variant picker hydrates.

We have a rule on every WitsCode build that removes the guesswork. If it can be Liquid, it must be Liquid. JavaScript is reserved for state that genuinely cannot exist at render time, and even in those cases, JavaScript is only allowed to swap fragments of Liquid-rendered HTML, never to build UI from raw JSON. This article is the full version of that rule. The reasoning, the cases where we still use JavaScript, the one Shopify feature that makes ninety-five percent of hydration unnecessary, and the SEO cost we keep watching clients pay when they ignore all of the above.

Why Server-Rendered HTML Beats Client-Side Hydration

Shopify's storefront is an unusually good server-rendering environment. Every storefront URL is cached at the edge by Shopify's CDN. Liquid renders inside Shopify's own template engine, in data-centre proximity to the product database, with every call to product, variant, metafield, and collection resolved before the HTML leaves the origin. By the time the browser receives the document, the work is done. There is no further round trip required to show product information. There is no hydration step required to make the page interactive for ninety-five percent of what users actually do on a Shopify storefront, which is reading, scrolling, and clicking links.

That architecture gives you four wins at once. Time to first byte is fast because the HTML is pre-assembled and cached. Largest contentful paint is fast because the hero image tag is in the document from byte zero and preload hints can be placed before any script runs. Interaction to next paint is protected because you have not shipped thousands of lines of framework code that must run before a click handler can execute. Cumulative layout shift is near zero because server-rendered HTML has known dimensions at first paint and nothing is being injected after the fact.

Move the same logic to JavaScript and every one of those wins flips. Time to first byte still looks acceptable because the shell HTML is small, but LCP regresses because your hero content is inside a React component that mounts after the bundle parses. INP regresses because the main thread is saturated by hydration on every interaction. CLS regresses because hydrated islands resize as they mount. The pattern is visible in Chrome User Experience Report data for Shopify stores that switched to heavily hydrated themes in 2024 and 2025. The stores that kept their product copy in Liquid consistently sit in the green band. The stores that moved to JS-heavy variant pickers and JS-rendered product descriptions sit at the yellow or red threshold on mobile.

The SEO Cost You Cannot Afford To Pay Twice

Googlebot has been evergreen Chromium since 2019, which meant for a while that client-rendered content was theoretically indexable. In practice, this was always a two-tier system. Google crawls the HTML immediately. Then, separately, pages go into a render queue where headless Chromium executes JavaScript and extracts the rendered DOM. That second pass has never been reliable, and in 2024 and 2025 Google has become more aggressive about deferring or skipping it on pages with limited crawl authority. For Shopify stores with thousands of SKUs, many of which will never accumulate the inbound links required to justify a second pass, the rendered DOM is a gamble.

We have taken over three different Shopify migrations in the last eighteen months where a previous developer had moved the product description to a client-side fetch against a PIM system. In every case, Google's indexed version of those product pages contained a skeleton and the word "Loading." Indexed word count hovered around forty. After moving the description into a metaobject and rendering it through Liquid, indexed word count climbed past six hundred within two crawls, and category rankings moved up within three weeks. No other change was made. The content had been there all along. Google just could not see it.

The problem compounds with AI crawlers. ChatGPT's browsing, Perplexity's fetcher, and most other AI agents do not execute JavaScript at all. Whatever is in the initial HTML response is what they ingest. Whatever is hydrated in later might as well not exist. If your product differentiation lives in a client-rendered spec table, you are invisible to the fastest-growing category of referral traffic. Shopify stores that keep their copy in Liquid are showing up in AI answers. Stores that hydrated their content are not.

The simplest diagnostic is to view source on a product page and search for a sentence from the product description. If it is in the HTML, you are safe. If it is not, you have a problem that no meta tag or schema markup can fix. A page with no text in the source is a page that might as well not exist to crawlers that skip the render pass.

The Section Rendering API Changes Everything

The single most underused feature in Shopify theme development is the Section Rendering API. Append ?sections=section-id to any storefront URL and Shopify returns a JSON payload whose keys are section identifiers and whose values are the fully rendered HTML of those sections. Not data. Rendered HTML. You can drop it directly into the DOM with innerHTML or replaceWith and you are done.

This is what the request looks like when you want to update a price and a buy button after a variant click. You fetch /products/handle?variant=12345&sections=price,buy-buttons, parse the JSON, and write the price key into the price container and the buy-buttons key into the buy-button container. Shopify has already applied every piece of Liquid logic server-side. Currency formatting, discount calculations, translation strings, metafield lookups, inventory thresholds, compare-at pricing, all of it has been resolved before the HTML leaves the origin. The browser does not have to know any of that logic exists. It just swaps a chunk of DOM.

The same mechanism works for the cart. /cart/update.js?sections=cart-drawer,cart-count,mini-cart returns the cart JSON alongside the rendered HTML of whichever sections you name. You update the cart state in one request, get the rendered HTML in the same response, and swap it in. Dawn does this throughout its cart drawer. Most custom themes we inherit do not, which is why they end up with a second set of cart-rendering logic written in JavaScript that duplicates and drifts from the Liquid version.

The strategic point is this. If you are writing a JavaScript component that fetches JSON from an API endpoint and assembles HTML from that JSON, you have almost certainly picked the wrong tool. Your Liquid already knows how to render the correct HTML. Your JavaScript only needs to ask Liquid to render it again. The Section Rendering API is the bridge. Once your team internalises this pattern, the number of places where you need true client-side rendering collapses to a small handful of edge cases.

The Two Places We Break The Rule

On every WitsCode theme, there are exactly two places where we intentionally let JavaScript do work that Liquid could theoretically do. Both exist because the Section Rendering API approach is either too slow for the interaction or because the content produced is ephemeral and non-indexable. We document these exceptions in every theme README so the rule remains the rule and the exceptions remain exceptions.

The first is predictive search. When a user types into the search box, the suggestion dropdown needs to update within roughly a hundred and fifty milliseconds of each keystroke. A full Liquid section render per keystroke is not feasible at that latency, and the content is not meant to be indexed anyway. We call Shopify's /search/suggest.json?q= endpoint and render the results into the dropdown with a small client-side template. The main /search results page itself, which is indexable and which users actually land on, is fully Liquid rendered. The dropdown is a UX enhancement, not an SEO surface. Where the ?sections=predictive-search variant gives us the layout we want, we prefer it, because rendered HTML is always less work than assembling from JSON. Where our custom layout requires raw JSON, we take the cost knowingly.

The second is the variant picker. When a user clicks a size or colour swatch, price, SKU, availability, selected image, and URL must update without a full page reload. A Liquid round trip via the Section Rendering API works, and we use it on slower pickers, but it adds roughly two to three hundred milliseconds on mid-tier connections, which is enough to feel unresponsive on a mobile product page. For variant selection we embed the product object as JSON at render time using {{ product | json }}, and a small JavaScript module mutates price, availability, and image state from that embedded data. The critical nuance is that the embedded JSON is a backup data source for the interaction only. The initial page render contains the fully resolved Liquid HTML for every piece of product information a crawler cares about. Title, description, price, specifications, reviews, and schema are all in the document from byte zero. JavaScript only runs after an interaction, and even then it mutates existing DOM rather than building new DOM from scratch.

Everything else on a Shopify storefront, every product card, collection grid, navigation menu, hero banner, review list, recommended-product carousel, press logo strip, FAQ accordion, size chart, shipping information block, and footer, is pure Liquid on our builds. If we catch ourselves reaching for a JavaScript solution for any of the above, we stop and ask why. The answer is almost always that we were copying a pattern from a JavaScript-first framework and forgetting that Shopify does not need it.

The Tests We Run Before Shipping

Three tests gate every theme we ship. The first is view source with the JavaScript disabled. If product copy, pricing, category description, blog content, or footer links are missing when JS is off, the theme fails review. The second is Lighthouse with throttling on mobile. We run against three product pages, three collection pages, and the homepage. If any of the six scenarios miss the green threshold on LCP or INP, we refactor the hydrated island that's pulling the numbers down. The third is the Chrome Theme Inspector flame graph against the slowest collection page. We want to see Liquid render time under two hundred milliseconds on a fifty-item grid, and we want to see no individual section or snippet eating more than thirty-five milliseconds.

If all three tests pass, the see-saw is balanced correctly. Liquid is doing the heavy lifting. JavaScript is only running for the ephemeral, interactive, genuinely client-only pieces of the experience. Crawlers are seeing the full page. Users are seeing fast paints and responsive interactions. The theme is cheap to maintain because there is no duplication between server-rendered templates and client-side views.

When You Need Help Rebalancing The See-Saw

Most of the themes we take over failed these tests on arrival, and the reason is usually the same. A developer familiar with React-first or Next.js patterns treated Shopify as an API backend, fetched product data client-side, and rebuilt the rendering layer in JavaScript. The result was a slower site, lower search visibility, higher maintenance cost, and a team that could no longer reason about which version of the truth the user was seeing. Rebalancing a theme like this is not a content edit. It is a structural rebuild of which layer owns which decisions.

WitsCode is the last-mile developer agency for Shopify merchants who have inherited a theme that leans too hard on JavaScript and are paying for it in Core Web Vitals, organic traffic, and conversion rate. Our Shopify theme engineering engagement rewrites the theme so logic lives in Liquid, JavaScript touches only the interactions it must, the Section Rendering API is used wherever a mutation is needed, and the product copy lives in the HTML document from byte zero where Google and every AI crawler can read it. Across more than two hundred and fifty sites we have shipped, this is the single change that moves the most metrics at once. If your storefront is hydrating content that should be static, we can rebalance it. Send us the URL and we will run the three tests above and show you exactly where the see-saw is tilted.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

Shopify Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss ecom for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.