Ask
108
@drainfield_dez ·

vue hydration text mismatch only in the production build, dev is clean SSR

Nuxt 3.15, Vue 3.5. Dev is completely fine. Deployed build gives me:

Hydration text content mismatch on <div> - rendered on server: 2 hours ago - expected on client: 3 hours ago

and then a chunk of the page re-renders and the scroll position jumps. It's a relative timestamp component. Obviously the server rendered at one time and the client at another, but I don't understand why dev never shows it and what the sanctioned fix is. Wrapping it in <ClientOnly> works but then every card shifts on load.

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @tomato_tobin · 7mo ago · 3 replies

    Dev never shows it because you're rendering per request against a server clock that's milliseconds from your browser clock. In production the page is cached — CDN, ISR, whatever you have — so the HTML is an hour old by the time a browser sees it. It isn't a Vue bug, your component genuinely isn't deterministic.

    The fix that avoids the shift: render the absolute timestamp on the server, upgrade to relative after mount.

    <script setup>
    const props = defineProps<{ iso: string }>()
    const rel = ref<string | null>(null)
    onMounted(() => { rel.value = formatRelative(props.iso) })
    </script>
    <template>
      <time :datetime="iso">{{ rel ?? formatAbsolute(iso) }}</time>
    </template>
    

    Server and client both produce the absolute string for first paint, so hydration matches. Then it swaps. No ClientOnly, and no shift if the element has a stable width.

    127
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @drainfield_dez · 7mo ago

      This worked and took ten minutes. The absolute-first render is also better for anyone landing with JS still loading.

      34
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @iris_tanaka · 7mo ago

      And better for search — a real <time datetime> with an absolute value in the HTML beats 'a while ago'.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @cookie_domain_al · 7mo ago

    General rule that's saved me a lot of time: anything that touches Date.now(), Math.random(), window, locale or timezone during render is a hydration mismatch waiting for the right cache configuration. Grep for those in your components and treat every hit as a decision you have to make.

    And whenever the answer to "why only in production" is unclear, ask how old the HTML is by the time the client sees it. That's usually the whole story.

    52
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @oncall_omar · 7mo ago · 2 replies

    Related trap: if you format with toLocaleString, the server's locale and timezone are not the user's. Server in UTC renders 14:00, browser in Berlin renders 16:00, mismatch. Pin the timezone explicitly on the server side or do the same mount-swap trick.

    28
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @drainfield_dez · 7mo ago

      We had this two components over. Same fix worked for both.

      14
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @teardown_tues · 7mo ago · 2 replies

    just suppress the warning, vue lets you ignore mismatches on a node. no need to restructure a component over a console message

    26
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @tomato_tobin · 7mo ago

      The warning isn't the problem, the re-render is. Vue patches the mismatched subtree, which is exactly why they're losing scroll position. Suppressing the message doesn't stop the patch, it stops you hearing about it.

      22
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @rubberduck_rae · 7mo ago

    If you keep ClientOnly anywhere, use its fallback slot with an element of exactly the same dimensions. Most "ClientOnly causes layout shift" complaints are really "ClientOnly rendered nothing, then something".

    13
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @edge_runtime_bo · 7mo ago

    Worth checking your actual cache TTL while you're in here. "rendered 2 hours ago" means something is serving HTML at least an hour stale, which may or may not be what you intended for that route.

    9
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report