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: List rendered components during SSR #5476

Closed
wants to merge 2 commits into from
Closed

Feature: List rendered components during SSR #5476

wants to merge 2 commits into from

Conversation

AlbertMarashi
Copy link

@AlbertMarashi AlbertMarashi commented Sep 30, 2020

Resolves issue: #4854

Done

  • Added tests
  • Added documentation

Feature

This feature adds a renderedComponents array with a list of filenames of components rendered during SSR.

You can match these filenames to client-side modules output by a bundler such as Rollup in order to preload necessary asychnronous components before the client-side realises it even needs it.

This feature is important for apps that use asynchronous components, and would like to ensure the client-side modules are loaded asynchronously instead of synchronously (waterfall effect)

Without this feature Total time: 2,161ms

image

With this feature Total time: 1,645ms
image

This time gains compound for each additional nested asychronous layer (eg: async components requiring async components).

Time savings also be improved by implementing HTTP/2 Push, which is currently not possible without this feature.


Use case:

This feature is essential for enterprise applications that would like to provide a fast client-side experience, with minimal loading times for asynchronous components.

Asset injection ensures that the necessary scripts are prefetched/preloaded for the client in parallel (and enables the possibility of http/2 PUSH for async output files)

image

@AlbertMarashi AlbertMarashi changed the title Display renderedComponents during SSR Feature: Display renderedComponents during SSR Sep 30, 2020
@AlbertMarashi
Copy link
Author

@pngwn @Rich-Harris @antony @allofthenamesaretaken @benmccann @mindrones

Can we get this added to the next release? Very essential feature for anyone who wants to speed up their application by being able be able to http/2 push or preload asychronous modules that have been rendered during SSR.

@benmccann
Copy link
Member

What's the reason for opening a new PR? What changed between this and #4856?

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Oct 1, 2020

Added tests & docs @benmccann

@allofthenamesaretaken
Copy link

Really need this feature ASAP for the current project I am working on in my startup. How soon before this is added to the next release?

@AlbertMarashi AlbertMarashi changed the title Feature: Display renderedComponents during SSR Feature: List rendered components during SSR Oct 2, 2020
@benmccann
Copy link
Member

Ok, thanks for the clarification. And thanks for adding tests! Just FYI, you can update existing PRs with changes like that and don't have to open new ones

@Rich-Harris
Copy link
Member

I have to admit this feels a bit like 'scenario solving'. The way we've solved this elsewhere is by analysing the SSR bundle alongside the route manifest that informs SSR. This has a key advantage over the approach outlined here:

<script>
  // Foo is a dependency of this component, and will need to be loaded
  // before this component can render...
  import Foo from './Foo.svelte';

  let condition = false;
</script>

<!-- ...but it isn't rendered during SSR, so won't show up in the list of necessary components -->
{#if condition}
  <Foo/>
{/if}

<button on:click={() => condition = true}>
  show foo
</button>

Using a route manifest is more work but more reliable, and doesn't increase API surface area.

@benmccann
Copy link
Member

I think the way you'd write that code sample would probably look more like:

<script>
  export let Foo;

  let condition = false;

  // Foo is imported dynamically only when needed
  if (condition) {
    import('./Foo.svelte').then(mod => {
      Foo = mod.default;
    });
  }
</script>

<!-- it likely will render Foo on the client only if it rendered on the server - though this is not guaranteed if the if statement checks for the presence of window or does something similar to specifically make the client-side rendering purposefully different -->
{#if condition}
  <svelte:component this={Foo}/>
{/if}

<button on:click={() => condition = true}>
  show foo
</button>

That makes the case for this feature a bit more reasonable. Whether it meets the bar for being worth an API addition though I'm not sure of. I've personally never run into a case where the manifest approach is not sufficient

What we want to do here may also be influenced by what we decide regarding browser vs server scripts (somewhat discussed in sveltejs/rfcs#27). The more browser and server logic diverge probably the less useful it is to load client-side scripts based upon what happened on the server

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Oct 20, 2020

Thank you for your comment @Rich-Harris

In the example you gave above, Foo.svelte would be automatically included in the bundle for that component's module since it's a normal import (not async).

If we call that component Page.svelte, you would be able to find it's filename in the renderedComponents. You link the filename of Page.svelte to an output module with your manifest file. This way you can know which specific module to load on the client-side (by knowing was rendered during SSR)

In an actual async example, (eg: Foo.svelte loaded asynchronously), it would result in a seperate module, and knowing which module to preload is practically impossible without having some sort of hint of what was rendered during SSR

The problem with a route-manifest is that it only works one layer deep (eg: Page level). If you try importing nested async components we won't have any idea of what to preload.

This would be especially useful for the {#await} syntax during SSR
#958.


There's also some scenarios where a route-manifest won't help you too much, consider you have a router that dynamically returns a different page component on the same url. Adding the logic to handle that stuff would be incredibly difficult, especially if there's lots of these types of routes

eg:

Imagine a news website that doesn't want to use subfolder prefixes for knowing what type of page to render. They may want to do this for SEO/UX purposes. (eg: /some-interesting-news instead of /article/some-interesting-news has some minor SEO benefits)

let pageTypes = {
    article: () => import('@/templates/article'),
    factcheck: () => import('@/templates/factcheck'),
    category: () => import('@/templates/category'),
}

let routes = [
    //catch all path
    new Path('/*', async ({slug}) => {
        let pageType = await getPageType(slug) //contact backend to find page type of this URL
        if(!pageType) return () => import('@/pages/404')

        return pageTypes[pageType] // this could return 1 of 3 PageTypes
    })
]

Your route-manifest won't help much here, as the URLs are dynamic.


Another use-case:
Imagine you have a dashboard app where you want to use SSR, the dashboard may be user-specific, meaning different widgets and components for different users. Imagine all of these dashboard components are asynchronously loaded (because as a user you don't want to load every dashboard component that you aren't using in your dashboard)

In this scenario, the dashboard components in this route are unknown until rendering, and you could only know which components to preload if an API such as this was exposed.

I don't like to create additional API surface, but this is a pretty essential feature that exists in other frontend frameworks.

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Nov 6, 2020

I have to admit this feels a bit like 'scenario solving'. The way we've solved this elsewhere is by analysing the SSR bundle alongside the route manifest that informs SSR. This has a key advantage over the approach outlined here:

<script>
  // Foo is a dependency of this component, and will need to be loaded
  // before this component can render...
  import Foo from './Foo.svelte';

  let condition = false;
</script>

<!-- ...but it isn't rendered during SSR, so won't show up in the list of necessary components -->
{#if condition}
  <Foo/>
{/if}

<button on:click={() => condition = true}>
  show foo
</button>

Using a route manifest is more work but more reliable, and doesn't increase API surface area.

Foo.svelte would not be included in the renderedComponents array, however, when using code-splitting, it would be included in the same chunk as the parent component, as it's being statically imported.

Even if it were asynchronously loaded (eg: import('Foo.svelte')) it's safe to assume that 95% of components rendered on the client-side would also be rendered during SSR. (Otherwise, there's not much point of using SSR)

The problem this is solving Time-till-interactive caused by loading async components on an already SSR'd page. In fact, what can happen is that the client hydration failure may cause the entire page to flash white until the async components load, this is of course bad UX.

Like you said, using a route manifest will involve more work, and this solution would save time and cover over 90% of use cases.

I am using this fork in 4 production builds because of it's loading speed & SEO benefits. I really love what Svelte is doing, and a feature like this will help mature it into a strong option for enterprises.

I plan to publicly release the router we have created using this system, which would help contribute to the svelte ecosystem. It's based on Vue's vue-router, which would help a lot of Vue users feel comfortable migrating to Svelte with relatively little effort.

We are using this feature for a SaaS startup my team and I are building. We would love to see it make the core :)

@Rich-Harris

@AlbertMarashi
Copy link
Author

Bump

I would love to see this merged so I can release my router I'm using across a few projects that does SSR & client-side routing module & supports HTTP2/Push.

@pngwn
Copy link
Member

pngwn commented Dec 13, 2020

I don't think we're at the state where we'd be merging this as there is no consensus among maintainers that this feature is required. We'll circle back on this as time allows.

@stale
Copy link

stale bot commented Jun 26, 2021

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale-bot label Jun 26, 2021
@stale stale bot removed the stale-bot label Jun 26, 2021
@stale stale bot removed the stale-bot label Jun 27, 2021
@benmccann
Copy link
Member

benmccann commented Jun 27, 2022

SvelteKit handles preloading pretty well. If you had dynamic imports this might allow them to be preloaded, which we can't do today as we don't know which ones actually get used

@dummdidumm dummdidumm added this to the one day milestone Mar 14, 2023
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 this pull request may close these issues.

6 participants