Skip to content

Async/streaming SSR renderer #958

Description

@Rich-Harris

Now that we have an await template primitive, it makes sense to have a streaming renderer:

require('svelte/ssr/register');
const app = express();

const SomeRoute = require('./components/SomeRoute.html');

app.get('/some-route', (req, res) => {
  SomeRoute.renderToStream({
    foo: getPromiseSomehow(req.params.foo)
  }).pipe(res);
});

It would write all the markup to the stream until it encountered an await block (at which point it would await the promise, and render the then or catch block as appropriate) or a component (at which point it would pipe the result of childComponent.renderToStream(...) into the main stream).

We'd get renderAsync for free, just by buffering the stream:

const markup = await MyComponent.renderAsync({...});

Doubtless this is slightly more complicated than I'm making it sound.

Activity

  1. JulienPradet commented on Jan 27, 2020

    @JulienPradet

    Would you consider a PR that would tackle renderAsync that handles {#await} but without the streaming approach ?

    The reason I'm suggesting this is because I don't know how to tackle head and css rendering in a streaming context. And a renderAsync would still be useful for Static Site Generators which don't need streaming since everything is done at build time.

    Unless you have some pointers/ideas that I could try to implement?

  2. AlbertMarashi commented on May 13, 2020

    @AlbertMarashi

    @Rich-Harris would it be appropriate to have some sort of serverPrefetch() function like VueJS inside components to do async data grabbing and provide state to the component

  3. AlbertMarashi commented on May 13, 2020

    @AlbertMarashi

    @JulienPradet I agree with you, I would prefer something that handles {#await} without the streaming approach.

    It wouldn't make sense as components contain their own head and CSS, and you would need to render all components before getting the head + css

  4. AlbertMarashi commented on May 13, 2020

    @AlbertMarashi

    Would it be possible to have some sort of onSSR() hook inside components that are called asynchronously on the serverside, and only available within the renderAsync() function?

    This is similar to what Vue does: https://ssr.vuejs.org/api/#serverprefetch & https://ssr.vuejs.org/guide/data.html#data-store

    I am looking into how we could better implement SSR & Async data in svelte. We need first-class SSR support if we want enterprises to use this.

  5. AlbertMarashi commented on May 13, 2020

    @AlbertMarashi

    I've helped a bit with Vue's server-side rendering tools, and have thought a bit about SSR and passing data to clients and have a few thoughts that could hopefully be considered

    Ideally, all data generated during SSR should be passed to the client, so that the client can have the same state, and stop client-side hydration from failing.

    Generally, some sort of global store can make this easy, as the state can be exported as JSON and injected on the client-side (tools like Vuex have .replaceState for hydration). One issue with this is that every component that needs data passed to the client needs to use a store, and can't really use local state. This is not great since you want your component to not rely on external variables

    My ideal setup/flow would work something like this:

    <script>
    let article
    
    // optional preload function called during SSR, available to all components
    export async preload({ params }){ 
      article = await getArticle(params)
    }
    </script>

    pseudocode during SSR

    // object containing all data for all components, serialized to JSON before sent to client
    let hydrationData = {} 

    I'm not sure about the exact logic and order of operations in svelte, but I imagine something like this

    // done for every component
    function create_ssr_component() {
       let component = instantiateComponent(Component)
       if (component.preload) { //update
         component.$set(await component.preload())
       }
       hydrationData[component.uniqueID] = component.$capture_state() //server generated unique id
       return await component.renderAsync() // data is captured before HTML string is generated
       //output { html: `<div ssr-id="1">...</div>`, ... }
    }

    Doing the rendering

    let { html, css, head, hydrationData } = await App.renderAsync()
    // window variable included inside <head> for client hydration
    `<script>window.__hydrationData__ = ${JSON.stringify(hydrationData)}</script>`

    on the client

    new App({
      target: document.querySelector('#app'),
      hydrationData: window.__hydrationData__,
      hydrate: true
    })
    

    On the client-side, the client will try to inject each component's hydrationData as props, matching the server-side generated unique IDs included on every component with the hydrationData, aborting hydration in any subtree where data doesn't make sense

    I hope this makes sense @Rich-Harris, I am sure there's many issues with this, but I feel that it could potentially work

    Alternatively

    Do something similar to vue, expose some sort of serverPrefetch hook on the server side, allow it to receive some sort of $ssrContext, like vue (which may include URL, req/res or params), and allow it to access some global store so state can be transferred to the client-side

    Right now

    I am relying on <script context="module">, and my router is awaiting the function's result prior to the route-level component being rendered, and exposing it to the prop.

    This isn't optimal, as I would like sub-components to have access to asynchronous data loading

    <script>
    export let test
    </script>
    <script context="module">
    export async function serverPrefetch () {
        return {
            test: await 'foo'
        }
    }
    </script>
  6. maxcoredev commented on Oct 3, 2020

    @maxcoredev

    Hi everyone, just wanted to share how I deal with it now.

    SSR is only for search engines, right?
    Then everything we need to stay with App.render is to implement sync fetch and omit async/await syntax while SSR (not a big problem since it is only for search engines based on robots http headers or special GET param).

    So, three things needed to make browser and server code act the same:

    1. Make https://www.npmjs.com/package/node-fetch accessable globally so no need to import
    2. Extend it with fake .then() method
    3. Remove async/await syntax using Sveltes preprocess

    Server config rollup.config.server.js:

    function sync(content) {
    	const compiler = require('svelte/compiler');
    	return compiler.preprocess(content, {
    		markup: ({ content }) => {
    			let code = content
    				.replace(/\basync\b/g, '')
    				.replace(/\bawait\b/g, '');
    			return { code };
    		}
    	});
    }
    export default {
        ...
    	output: {
            ...
    		intro: `const fetch = require('sync-fetch');
    		        fetch.Response.prototype.then = function(foo) {
    		        	return foo(this);
        			}`
    	},
    	plugins: [
    		svelte({
            	...
    			preprocess: {
    	        	script: ({content}) => sync(content),
    			}
    		}),
    	],
    }
    

    Now we can write clear pragmatic code that will produce exact same result on both SSR and browser

    Now, this works:

    <script>
        let objects = fetch(`http://localhost:8000/api/article/1/`).then(r=>r.json());
    </script>
    
    {#await object then object}
        {object.text}
    {/await}
    

    This also works:

    <script>
        let object;
        (async (page) => {
            objects = await fetch(`http://localhost:8000/api/article/1/`).then(r=>r.json());
        })()
    </script>
    
    {object.text}
    

    Does not work, but that's not a disaster: (probably can be easily solved somehow)

    <script>
        let object;
        fetch(`http://localhost:8000/api/article/1/`).then(r=>r.json()).then(r=>object=r)
    </script>
    
    {object.text}
    
  7. blairn commented on Oct 3, 2020

    @blairn

    SSR isn't only for search engines.

    A lot of the SSR we do is so people who have awful connections can still see our data visualizations while waiting for the rest to load - or if they don't have Javascript.

    Rendering a component on the server, and then using that component in the client is very useful. I just wish it was easier to wire it all up.

  8. AlbertMarashi commented on Oct 3, 2020

    @AlbertMarashi

    SSR can reduce the largest contentful paint, which is also an important UX factor that can affect perceptions of how fast websites load

  9. maxcoredev commented on Oct 3, 2020

    @maxcoredev

    Sry, I am not about SSR in general. Just about case of this thread

  10. roblabla commented on Jul 12, 2021

    @roblabla

    I'm trying out svelte/sveltekit in a small app I'm writing, and I'm hoping to query the database from the server during server-side rendering, and am having trouble understanding how I can do that without something similar to the preload function proposed in this comment.

  11. kawpii commented on Sep 15, 2021

    @kawpii

    @AlbertMarashi What is SSR strategy used in Svelte kit? Does it support streaming?

  12. y0zong commented on Jun 10, 2022

    @y0zong

    does this dead cuz it's years still in pending, and I am face this too, how to get render data after promise resolved if we not use sveltekit ?

  13. kryptus36 commented on Jun 11, 2022

    @kryptus36

    In sveltekit, this seems to have been solved using load()

    But for those of us not using sveltekit, is there a way to do it?

  14. Evertt commented on Jul 1, 2023

    @Evertt

    @Rich-Harris could we then also get "top-level awaits" in reactive blocks?

    <script>
        let name = "john"
        let upper
        $: {
            await sleep(3000)
            upper = name.toUpperCase()
        }
    </script>
    
    <input bind:value={name} />
    
  15. lucidNTR commented on Nov 28, 2023

    @lucidNTR

    adding more context for a usecase where sveltekits load does not help: i figure out what data needs to be loaded by looking at the property acces to stores that the templates do and then generate a query behind the sccene, so it is inherently async. corrently the only two options i have is to render twice once to see what data the template needs and again with the fetchedd data or to push nearly mepty SSR pages to the client and let everything be rendered client side but with streaming /async support i could use this for fully serverside rendering without any boilerplate manually defining what deata is needed.

  16. crisward commented on Feb 10, 2024

    @crisward

    I realise this issue was opened 7 years ago. But I'd love to see this in svelte 5.1. (Think you have enough headaches for 5). Not sure if the rewrite makes this any easier, but being able to async render on the server would be a killer feature.

  17. added this to the 5.x milestone on Apr 1, 2024
  18. AlbertMarashi commented on Apr 17, 2024

    @AlbertMarashi

    I have this usecase where I am dynamically loading SVG icons by name from an API endpoint.

    Unfortunately, it doesn't render in SSR

    <script lang="ts">
    import PlaceholderIcon from "./icons/PlaceholderIcon.svelte"
    
    export let name: string
    
    async function load_icon(name: string) {
        const res = await fetch(`/api/icon/${name}`)
        const text = await res.text()
        return text
    }
    
    $: icon_svg = load_icon(name)
    </script>
    {#await icon_svg}
        <PlaceholderIcon/>
    {:then svg}
        {@html svg}
    {/await}

    Would be pretty sweet to have async components for this sort of use case (or like being able to top-level await in components)

  19. JonathonRP commented on Sep 7, 2024

    @JonathonRP

    This would be great. it's a bridge that'll have to be crossed by svelte eventually. Especially with react handling this use case. It would be nice to have a streaming response too for svelte kit which could enable calling and loading data through function calls like react 19

  20. added a commit that references this issue on Dec 24, 2024
  21. AlbertMarashi commented on Jan 17, 2025

    @AlbertMarashi
  22. Ocean-OS commented on May 1, 2025

    @Ocean-OS
    Member

    It seems relevant to tag #15844/#15845 here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions