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

Feature: render HTML without hydration #14337

Closed
trueadm opened this issue Nov 17, 2024 · 9 comments
Closed

Feature: render HTML without hydration #14337

trueadm opened this issue Nov 17, 2024 · 9 comments
Assignees

Comments

@trueadm
Copy link
Contributor

trueadm commented Nov 17, 2024

It can be beneficial to render HTML from Svelte without the need to handle hydration – for things such as email content, CMSs, RSS feeds and other such cases.

Rather than being an option we pass in to render from svelte/server, we should likely use another API here to enable future compatibility with possible async functionality, thus making this an API you should await (similar to what React does for this) as without hydration, showing async loading content would be useless. This change would meant that await blocks would no longer show their pending states and instead wait until the promise resolves.

import { renderStaticHTML } from 'svelte/server';
import App from './App.svelte';

const result = await renderStaticHTML(App, {
	props: { some: 'property' }
});
result.body; // HTML for somewhere in this <body> tag
result.head; // HTML for somewhere in this <head> tag

For this I propose renderStaticHTML but maybe there's a better name out there.

@Azarattum
Copy link
Contributor

I like the idea, but I don't really get how the proposed API would work. If I understand correctly, render from svelte/server already renders completely static HTML. Whether to hydrate it or not is the responsibility of the client APIs. I think having something like

<script>
	let { children } = $props();
</script>

{@render.static children?.()}

would make more sense. In that case, server generated code would be exactly the same as @render. Meanwhile the client code would just skip the nodes without any hydration. Probably some kind of comment marker would be needed for that to work.

This approach could allow for partial hydration/server components to be somewhat possible in Svelte.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 18, 2024

@Azarattum render generates HTML that needs to be hydrated. renderStaticHTML generates HTML that is never interactive, they're too very different APIs.

server generated code would be exactly the same as @render. Meanwhile the client code would just skip the nodes without any hydration. Probably some kind of comment marker would be needed for that to work.

Partial hydration offers very little to no benefit either – it just moves work, it doesn't avoid it.

@Azarattum
Copy link
Contributor

I think I still don't quite understand what you mean. Let's take an example:

<script>
  let count = $state(0);
</script>

{#snippet example()}
  <button onclick={() => count++}>Count: {count}</button>
{/snippet}

{@render example()}

The generated server code for this is:

import * as $ from "svelte/internal/server";

export default function App($$payload) {
  let count = 0;
  
  function example($$payload) {
    $$payload.out += `<button>Count: ${$.escape(count)}</button>`;
  }
  
  example($$payload);
}

Which means that render(example) will produce <button>Count: 0</button> on the server. I don't get what we could change in this output to make it "less interactive", since without hydration it does nothing anyway.

Now, let's look at the client code generated with @render by the compiler:

import * as $ from "svelte/internal/client";

var on_click = (_, count) => $.update(count);
var root_1 = $.template(`<button> </button>`);

export default function App($$anchor) {
  const example = ($$anchor) => {
    var button = root_1();
    button.__click = [on_click, count];
    var text = $.child(button);
    
    $.reset(button);
    $.template_effect(() => $.set_text(text, `Count: ${$.get(count) ?? ""}`));
    $.append($$anchor, button);
  };
  
  let count = $.state(0);
  example($$anchor);
}

$.delegate(["click"]);

This code is what make the component interactive.


Now, let's imagine how the generated code would work for @render.static:

import * as $ from "svelte/internal/server";

export default function App($$payload) {
  let count = 0;
  
  function example($$payload) {
    $$payload.out += `<button>Count: ${$.escape(count)}</button>`;
    $$payload.out += '<!--end-marker-->';
  }
  
  example($$payload);
}

and on the client (assuming unused variables and functions have been tree-shaken):

import * as $ from "svelte/internal/client";

var root = $.server_template(`<!--end-marker-->`);

export default function App($$anchor) {
  const example = ($$anchor) => {
    var server_template = root();
    $.append($$anchor, server_template);
  };
  
  example($$anchor);
}

In this case render(example) on the server would produce <button>Count: 0</button><!--end-marker--> and on the client the template is left as is and hydration is skipped. So, there is no need to change the render function itself (apart from the async features).

P. S. $.server_template in my example would skip the hydration of all the siblings until the <!--end-marker--> comment.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 18, 2024

How does the compiler know that when compiling that component that it’s only referenced from a render.static - the hydration flag would still be in the bundle because of other code, so tree shaking can’t do this for you.

Also, the attachment of event listeners needs ro happen otherwise it won’t be interactive, no?

@Azarattum
Copy link
Contributor

A component doesn't need to know how it's referenced. There is no hydration flag. @render.static doesn't care what it renders on the client. It just skips over whatever has came from the server until the marker comment.

So the ServerOnlyComponent wouldn't even be in the client bundle at all.

{#snippet example()}
  <ServerOnlyComponent/>
{/snippet}

{@render.static example()}

Since instead of:

import * as $ from "svelte/internal/client";
import ServerOnlyComponent from "./ServerOnlyComponent.svelte";

export default function App($$anchor) {
	const example = ($$anchor) => {
		ServerOnlyComponent($$anchor, {});
	};

	example($$anchor);
}

I would generate:

import * as $ from "svelte/internal/client";
import ServerOnlyComponent from "./ServerOnlyComponent.svelte"; // this will tree-shake

export default function App($$anchor) {
	const example = ($$anchor) => {
		var server_template = $.server_template(`<!--end-marker-->`); // it could be just $.server_template() if the marker is static
                $.append($$anchor, server_template);
	};

	example($$anchor);
}

or even

import * as $ from "svelte/internal/client";
import ServerOnlyComponent from "./ServerOnlyComponent.svelte"; // this will tree-shake

export default function App($$anchor) {
	$.server_template($$anchor);
}

otherwise it won’t be interactive, no?

@render.static on the client just wouldn't care what's inside it. And the whole point is that it won't be interactive.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 19, 2024

What I'm saying is that it's possible to have those characteristics without introducing a new API. However, that's unrelated to this PR.

@Azarattum
Copy link
Contributor

possible to have those characteristics

How would we do that exactly? The closest thing we have right now is:

{@html import.meta.env.SSR ? render(someComponent).body.replaceAll("<!---->", "<!--~-->" : " "}

And it has 2 major downsides. The replaceAll hack because of #14323. And the fact that after rerendering on the client (e.g. in if block) the server HTML is lost and replaced with whatever we've put in the ternary (" " in this case). I would imaging these issues to be addressed by @render.static. If there is another way, I would like to know.

I agree that this would be out of scope for this PR. Though as it was pointed out here #14323 (comment) , this PR in its current form wouldn't solve the #14323 .

@trueadm
Copy link
Contributor Author

trueadm commented Nov 19, 2024

How would we do that exactly? The closest thing we have right now is:

It's not available right now, but it's on our roadmap for next year.

I agree that this would be out of scope for this PR. Though as it was pointed out here #14323 (comment) , this PR in its current form wouldn't solve the #14323 .

It would solve part of the problem around hydration markers, but agreed it won't solve it all. It might be better to have these discussions on that issue rather than this PR though.

@trueadm
Copy link
Contributor Author

trueadm commented Nov 19, 2024

We've decided against introducing a new server API for now.

@trueadm trueadm closed this as not planned Won't fix, can't repro, duplicate, stale Nov 19, 2024
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

Successfully merging a pull request may close this issue.

3 participants