Vue Storefront SSR Hydration Mismatches: Diagnosis and Fixes
Hydration mismatches break Vue Storefront SSR pages. Use this cause table and ordered checks to separate browser-global bugs, client-only data and stale service workers.
24 Jul 2026, 13:36 UTC

What a hydration mismatch looks like
With server-side rendering enabled, Vue Storefront (the Nuxt 2 based 2.x line) renders each route on the Node server, sends the HTML, and then hydrates it: Vue attaches event listeners and takes over the existing DOM instead of rebuilding it. Hydration only succeeds if the client's first render produces the same tree the server produced. When it does not, Vue 2 logs a warning along the lines of The client-side rendered virtual DOM tree is not matching server-rendered content and patches the difference. What you see as a user is a flash of changed text, a duplicated widget, or a component that looks correct but ignores clicks until a full reload.
This guide targets Vue Storefront 2.x on Nuxt 2.x with Vue 2. Nuxt 3 / Vue 3 reports the same class of problem with different wording; the checks below still apply, but the component APIs differ.
Cause table: match the symptom before changing code
| What you see | Likely cause | Where to look | Fix direction |
|---|---|---|---|
| Warning names a component whose text changes right after load | Non-deterministic value rendered in the template: new Date(), Date.now(), Math.random(), or timezone/locale formatting |
The component named in the warning stack | Render a stable placeholder on the server and fill the real value after hydration |
| Mismatch only for signed-in users | localStorage, sessionStorage or a cookie read during render |
Search the component tree for storage access | Move the read into mounted, or guard with process.client |
| Appears only after a deploy, disappears on hard reload | Service worker serving an HTML shell cached from the previous build | DevTools → Application → Service Workers; the @nuxtjs/pwa workbox runtimeCaching rules |
Serve navigation requests network-first, or version the service worker |
| Mismatch with no dynamic value in sight | Invalid HTML nesting (a block element inside <p>, nested <a>); the browser reparses the server HTML into a different tree |
Compare View Page Source with the Elements panel | Correct the template nesting |
| Product or price list is empty in the server HTML, filled on the client | Data fetched in mounted instead of asyncData / fetch |
The page component's data hooks | Move the fetch to asyncData or fetch so it runs server-side |
Ordered checks
- Reproduce cleanly. Use a fresh browser profile with extensions disabled. Analytics, A/B testing tools and extensions inject DOM before hydration and produce mismatches that never occur in a clean session.
- Capture both trees. Right-click → View Page Source shows the HTML the server sent. The Elements panel shows the post-hydration DOM. Diff the region the warning names; a text difference points at cause 1, a structural difference at cause 4.
- Read the warning stack. Vue names the component that failed to match. If the name is a library component you do not own, jump to the escalation criteria below.
- Grep for browser globals. From the project root (read-only, no permissions beyond file access):
Adjust the paths if your theme lives elsewhere. Any hit inside a render path is a candidate.grep -rn "window\.\|document\.\|localStorage\|Math.random\|Date.now" components/ pages/ layouts/ - Unregister the service worker and retest. If the mismatch vanishes, the cause is the cached shell, not your components.
- Confirm the server rendered content at all. With the dev server running on its default port:
Expected check: the output contains real product or category markup and meta tags. If the body is essentially an emptycurl -s http://localhost:3000/ | head -c 2000<div id="__nuxt">, SSR is not producing content for that route and the mismatch is a symptom rather than the cause.
Fixes tied to each finding
Browser global in a render path. Keep the server and client first render identical, then update after hydration:
data() {
return { isMobile: false }; // same value on server and client
},
mounted() {
// Runs only in the browser, after hydration.
this.isMobile = window.matchMedia('(max-width: 768px)').matches;
}
For a whole subtree that is meaningless on the server, Nuxt 2.9+ provides <client-only> (the replacement for the older <no-ssr>):
<template>
<div>
<!-- Nothing on the server; filled in after hydration -->
<client-only>
<LocalTime :timestamp="order.createdAt" />
</client-only>
</div>
</template>
Data fetched on the client only. Move the call into a hook that runs on the server:
// pages/Product.vue
export default {
// Runs on the server during SSR, then again before hydration.
async asyncData({ $api, route }) {
const product = await $api.getProduct(route.params.slug);
return { product };
}
};
Replace $api with the client your integration injects. The key is integration-specific, so check the plugin registration in your project or the integration's documentation rather than copying a name from another storefront.
Stale service worker. In the @nuxtjs/pwa workbox configuration, make navigation requests network-first so a new deploy is picked up, and version or skip-waiting the worker. This also addresses the cache-invalidation problem that appears when product data changes faster than the cached shell.
Verifying the fix
- Run a production build and start the server (check your
package.jsonscripts for the exact names; a Nuxt 2 project typically exposesbuildandstart). - Reload with the console open. The hydration warning should be gone and the console free of Vue warnings.
- Re-run the
curlcheck and confirm the server HTML still contains the product title and meta description. - A Lighthouse SEO audit is a coarse confirmation that the server HTML carries content. It will not detect hydration mismatches, so do not treat a passing score as proof.
- Nuxt suppresses some warnings in production builds, so verify in dev mode as well.
Escalation criteria
Stop changing your own components and escalate when: the warning names a third-party or platform component you do not control; the mismatch reproduces only in a production build or behind a CDN; or it survives unregistering the service worker and clearing the cache. Collect the Vue Storefront, Nuxt and Vue versions from package.json, the exact warning text and stack, the server HTML and client DOM for the affected region, whether a service worker was active, and a minimal reproduction. That package is what an integration maintainer needs to act.
Limitations
- With
ssr: trueand a Node server, HTML is generated per request and is not written to adistfolder. A missing pre-rendered file there is not evidence that SSR is broken; that check belongs to static generation (nuxt generate). - Cache purge and CLI command names differ between Vue Storefront versions and integrations. Confirm them from your own
package.jsonscripts and integration docs instead of assuming a command exists. - Newer releases of the platform use a different stack from the Nuxt 2 line described here; their hydration diagnostics and component APIs differ.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.