Skip to content

Excessive hydration markers on Svelte 5 #15200

@martgnz

Description

@martgnz

Describe the bug

After updating our template to Svelte 5 we are seeing an excessive amount of hydration markers, way more than in a default SvelteKit project. Our CMS has a hard limit on the size of the HTML strings — before gzip — and with so many markers it is easy to surpass it and be blocked from publishing. With adapter-static, a minimal Svelte 4 HTML payload is 5.7kb. With the same code, a Svelte 5 payload is 6.8kb, an increase of 18%. This change also makes debugging on the browser inspector very difficult.

We observe roughly 25 hydration markers between each node. In a baseline SvelteKit project (https://stackblitz.com/edit/sveltejs-kit-template-default-9a5meubq?description=The%20default%20SvelteKit%20template,%20generated%20with%20create-svelte&file=README.md&title=SvelteKit%20Default%20Template) we only see one marker between each component.

We understand that the hydration markers are part of the built output, as described in #14004 and #14099, but we believe there is something in how we’re rendering our components that is increasing the number of markers to a zany level, and we would be grateful if you could provide any suggestions or guidance on how to mitigate the issue.

At the moment, given the nature of our downstream consumers (iOS and Android applications), this is a blocker for us for moving to Svelte 5 at The New York Times. We’d be open to any suggestions on how best to approach!

Reproduction

Our current approach

Our template works with a body loop. The data comes from an external source and when the type matches a component, it renders. We also have a special component that imports and renders a component declared on a prop (saves you from importing it manually and adding it to the body loop), and it's imported using a +page.js file.

It looks roughly like this:

<script>
 import Text from './Text.svelte';
 import Header from './Header.svelte';
 import Rule from './Rule.svelte';
</script>


{#each data.body as { type, value: props }}
 {#if type === 'text'}
   <Text {props} />
 {:else if type === header}
   <Header {props} />
 {:else if type === rule}
   <Rule />
 {:else if type === 'svelte'}
   <svelte:component this={props.component} />
 {/if}
{/if}

Here's a sample REPL with a small test case where you can see the proliferation of markers. Our template is far more complex and has lots of components (and components inside components), but this illustrates the basic problem:

https://stackblitz.com/edit/sveltejs-kit-template-default-py8cc6sm?file=src%2Froutes%2F%2Bpage.svelte

Dynamic body loop

We also tried switching to a "dynamic" body loop that uses <svelte:component> to render every element based on an object. We thought this would reduce the markers since there are only two pairs of ifs in the loop. After testing, it does reduce the markers but it is still problematic. We are seeing ~10 markers between each node, which unfortunately it's not really usable either.

https://stackblitz.com/edit/sveltejs-kit-template-default-py8cc6sm?file=src%2Flib%2FNav%2Findex.svelte,src%2Froutes%2F%2Bpage.svelte,src%2Froutes%2Fdynamic%2F%2Bpage.svelte

<script>
import Code from '$lib/Code/index.svelte';
import DynamicComponent from '$lib/DynamicComponent/index.svelte';
import Header from '$lib/Header/index.svelte';
import Rule from '$lib/Rule/index.svelte';
import Text from '$lib/Text/index.svelte';

const components = {
 Code,
 DynamicComponent,
 Header,
 Rule,
 Text,
};

export let data;
</script>

<section>
 {#each data.body as block}
   {#if components[block.type]}
     <svelte:component this={components[block.type]} value={block.value} />
   {:else}
     Missing component
   {/if}
 {/each}
</section>

The data that renders this looks like this:

{
   "type": "Header",
   "value": "Svelte 5 hydration markers (dynamic components)"
 },
 {
   "type": "Text",
   "value": "In this example every component is imported statically and rendered without a body loop. There are still too many markers between elements."
 },
 {
   "type": "Rule",
   "value": ""
 },
 {
   "type": "Text",
   "value": "Quis nostrud <i>exercitation</i> ullamco laboris nisi ut aliquip ex ea commodo consequat."
 },
 {
   "type": "DynamicComponent",
   "value": "my prop"
 },
 {
   "type": "MyMissingComponent",
   "value": "my prop"
 }
]

Logs

Here's an example of the markup we're seeing with our current body loop:

<!--[-->
 <!--[-->
 <!---->
 <!---->
 <!--[-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[-->
 <div style="--g-header-text-wrap: balance;" class="g-header-container g-theme-news g-align-center g-style-default s-WxiNLIoGK6tg"><header class="g-header s-WxiNLIoGK6tg">
<!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <div class="g-heading-wrapper s-WxiNLIoGK6tg"><h1 class="g-heading s--WEV63UuptOE">
<!--34bvmx-->
   Lorem Ipsum Dolor!!
<!---->


 </h1>
<!---->
 </div>
<!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <!--[-->
   <div class="g-byline-wrapper s-WxiNLIoGK6tg"><p class="g-byline s-oyjqywUozi8_">
<!--[!-->
   <!--]-->
    <!--[!-->
   <!--[!-->
   <span class="g-byline-prefix s-oyjqywUozi8_">By</span> <span itemprop="name" class="g-last-byline s-oyjqywUozi8_">The New York Times</span>
<!--]-->
   <!--]-->
    <!--[!-->
   <!--]-->
 </p>
<!---->
    <span class="g-timestamp-wrapper s-WxiNLIoGK6tg"><time class="g-interactive-timestamp s-w590EQv4ALB0 " datetime="2024-12-02T16:30:17-05:00">
<!--[!-->
   <!--[!-->
   <!--[-->
   Dec. 2, 2024
<!--]-->
   <!--]-->
   <!--]-->


 </time>
<!---->
 </span></div>
<!--]-->
    <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
 </header></div>
<!---->
 <!---->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[-->
 <!--[-->
 <!--[!-->
 <!---->
 <p class="g-text  s-BKgJCsuAs_Ng">
<!--[-->
<!--om3kqk-->
Nisi aliquip mollit aliqua non in in, mollit in qui id enim reprehenderit adipiscing ea. Minim sit, tempor consequat occaecat ut sed reprehenderit in dolore cillum laboris culpa irure. Nulla amet sint do dolore ut quis eu officia minim in esse.
<!---->
<!--]-->
 <!---->
 </p><!---->
 <!--]-->
 <!--]-->
 <!---->
 <!---->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!---->
 <!---->
 <!---->
 <!--]-->
  <!--[!-->
 <div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
<!---->
 </div>
<!--]-->
<!--]-->


And this is what we see with the dynamic body loop:


 <!--[-->
 <!--[-->
 <!---->
 <!---->
 <!--[-->
 <!--[-->
 <!---->
 <div class="g-header-container g-theme-news g-align-center g-style-default s-WxiNLIoGK6tg" style="--g-header-text-wrap:balance">
   <header class="g-header s-WxiNLIoGK6tg">
   <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <div class="g-heading-wrapper s-WxiNLIoGK6tg">
   <h1 class="g-heading s--WEV63UuptOE">
   <!--34bvmx-->
   Lorem Ipsum Dolor
<!---->
  
 </h1>
 <!---->
 </div>
    <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <!--[-->
   <div class="g-byline-wrapper s-WxiNLIoGK6tg">
   <p class="g-byline s-oyjqywUozi8_">
   <!--[!-->
   <!--]-->
    <!--[!-->
   <!--[!-->
   <span class="g-byline-prefix s-oyjqywUozi8_">
   By</span>
    <span itemprop="name" class="g-last-byline s-oyjqywUozi8_">
   The New York Times</span>
   <!--]-->
   <!--]-->
    <!--[!-->
   <!--]-->
 </p>
   <!---->
    <span class="g-timestamp-wrapper s-WxiNLIoGK6tg">
   <time class="g-interactive-timestamp s-w590EQv4ALB0 " datetime="2025-01-31T14:03:50-05:00">
   <!--[!-->
   <!--[!-->
   <!--[-->
   Jan. 31, 2025<!--]-->
   <!--]-->
   <!--]-->
  
 </time>
 <!---->
 </span>
 </div>
   <!--]-->
    <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
    <!--[!-->
   <!--]-->
 </header>
</div>
 <!---->
 <!---->
 <!--]-->
 <!--[-->
 <!---->
 <!--[-->
 <!--[!-->
 <!---->
 <p class="g-text  s-BKgJCsuAs_Ng">
   <!--[-->
   <!--1r2ynjb-->
   hello<!---->
   <!--]-->
  
 <!---->
  
 </p>
 <!---->
 <!--]-->
 <!--]-->
 <!---->
 <!---->
 <!--]-->
 <!--[-->
 <!---->
 <!--[-->
 <!--[!-->
 <!---->
 <p class="g-text  s-BKgJCsuAs_Ng">
   <!--[-->
   <!--u0i1n6-->
   Et consequat laborum commodo aliqua eu in adipiscing incididunt ut, tempor amet ullamco dolore. Ex sunt mollit sunt ut veniam est dolore magna.<!---->
   <!--]-->
  
 <!---->
  
 </p>
 <!---->
 <!--]-->
 <!--]-->
 <!---->
 <!---->
 <!--]-->
 <!--]-->
 <!---->
 <!---->
 <!---->
 <!--]-->
  <!--[!-->
 <div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
  
 <!---->
  
 </div>
 <!--]-->
 <!--]-->

System Info

System:
	OS: macOS 14.7.2
	CPU: (10) arm64 Apple M1 Pro
	Memory: 361.97 MB / 32.00 GB
	Shell: 5.9 - /bin/zsh
  Binaries:
	Node: 22.13.1 - ~/.nvm/versions/node/v22.13.1/bin/node
	npm: 10.9.2 - ~/.nvm/versions/node/v22.13.1/bin/npm
	pnpm: 7.33.2 - ~/Library/pnpm/pnpm
  Browsers:
	Chrome: 132.0.6834.160
	Safari: 18.2

Severity

blocking an upgrade

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions