Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Excessive hydration markers on Svelte 5 #15200

Open
martgnz opened this issue Feb 3, 2025 · 8 comments
Open

Excessive hydration markers on Svelte 5 #15200

martgnz opened this issue Feb 3, 2025 · 8 comments

Comments

@martgnz
Copy link

martgnz commented Feb 3, 2025

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

@khromov
Copy link
Contributor

khromov commented Feb 4, 2025

👋 Thanks for making a reproduction. Have you tried comparing the size of the gzipped output between having the hydration markers and not having them? From my testing, gzip removes >90% of the extra size that hydration markers add. So while unsightly, it does not lead to increased payload size over the network.

@samjacoby
Copy link

The unfortunate limitation we're dealing with, is not the size over the wire — it's that our Android and iOS native applications have limited quotas for the final payload. We inline the HTML bodies of our interactive within a native webview context, which has some strict legacy inline size limitations. Linked resources and network size don't matter, in this case.

For what it's worth, I think what we're hoping to understand is how and why hydration markers are generated; it seems that something about the way that we author our components and application structure is creating more than would be typical.

@dummdidumm
Copy link
Member

We can definetly shrink this down I think. In your case you're getting a lot of markers from the if-else-if block chains, I'm currently investigating shrinking this down to two nodes per if block, regardless of how many else-if branches it has.

@Rich-Harris
Copy link
Member

I also wonder if we could collapse repeated comments, like instead of this...

<!---->
 <!---->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--]-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[!-->
 <!--[-->
 <!--[-->
 <!--[!-->

...maybe we could do this:

<!--2-->
<!--9]-->
<!--10[!-->
<!--2[-->
<!--[!-->

Need to be mindful of whitespace, of course.

@singlyfy
Copy link

singlyfy commented Feb 6, 2025

are the comments only viable option? I was thinking of data-attributes on nodes..that would be a bit "cleaner"

@paoloricciuti
Copy link
Member

are the comments only viable option? I was thinking of data-attributes on nodes..that would be a bit "cleaner"

there might not be an element since a component could be text only for example...but also that would not solve the size problem since data- is already two chars shy of <!---->...and adding a data attribute would actually be accessible from javascript.

@Ice-mourne
Copy link

You can improve dev experience by disabling HTML comments in the Chrome browser. I am not sure if Firefox has that option
Open console > CTRL + SHIFT + P > type "HTML comments" > press enter
Or Open console > go to settings (gear well on the top right of console) > and uncheck "show HTML comments"

@jdkdev
Copy link

jdkdev commented Feb 10, 2025

It seems like the issue has been identifed , but to just add that we are getting a TON too. And it must be from this and a few other things. If you needed another example for test cases.

<DefaultLayout>
  {#if banner}
    <slot name="banner">
      <Banner {banner} />
    </slot>
  {/if}
  {#if header}
    <slot name="header">
      <Header />
    </slot>
  {/if}
  <main class="{cls}">
    {#key $page.url}
      <slot name="hero">
        <PageHero />
      </slot>
    {/key}
    <slot />
  </main>
  {#if sidebar}
    <slot name="aside">
      <aside>
        <Sidebar />
      </aside>
    </slot>
  {/if}
  <slot name="shin">
  </slot>
  {#if footer}
    <slot name="footer">
      <Footer />
    </slot>
  {/if}
</DefaultLayout>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants