Skip to content

Top-level await #5501

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

Open
joas8211 opened this issue Oct 5, 2020 · 51 comments
Open

Top-level await #5501

joas8211 opened this issue Oct 5, 2020 · 51 comments

Comments

@joas8211
Copy link

joas8211 commented Oct 5, 2020

This is continuation for #5351.

The problem is that there's no way to render an asynchronously loaded component on SSR. My use-case for asynchronously loaded components is rendering JAM-stack site's content from JSON files. My JSON-files describe page content as blocks, that are different components (type) and their props (data).

I want to have top-level await support for component's <script> that will allow awaiting for promises before component's initialization is finished.

I've done an implementation for this, but changing initialization asynchronous is quite a breaking change. After the change it's not possible to initialize component with constructor, but instead we have to use a static async builder method Component.init(options). Not being able to initialize synchronously breaks custom elements. Component updating also becomes asynchronous so assignments to props don't get reflected to DOM synchronously. That will also break a lot of code.

If I make async initialization a compiler option, so that it doesn't trash backwards compatibility, would maintainers be willing to merge the changes? Is there any demand for this feature?

@antony
Copy link
Member

antony commented Oct 15, 2020

Top level await I believe is something which would be provided by acorn, not something we would try to implement ahead of it. Once acorn supports top level await then we will gain support for it.

@joas8211
Copy link
Author

@antony What do you mean? Acorn already supports "top-level await" with allowAwaitOutsideFunction: true. And I'm not trying to have a real top-level await (top-level of module). I'm just trying to have await for component's script-tag (init script) that is actually wrapped in a function that can be changed to an async function just like I did in my fork.

@probablykasper
Copy link

probablykasper commented Apr 12, 2021

@antony Any word on what Jesse said?

My use case: I have a module that loads some data asynchronously, which is then used by a bunch of components. All those components are shown to the user only after the data is loaded. If I make the loading async, it requires me to make a lot of changes to make it work, whereas a top-level await would mean I just put a single await statement in the necessary components.

@frederikhors
Copy link

I opened this on kit. Same question: sveltejs/kit#941.

How to use acorn options?

@joas8211
Copy link
Author

@frederikhors I think you cannot change how the Svelte compiler parses JavaScript without modifying the compiler. And the Acorn flag alone wouldn't do you any good. The component script must be wrapped inside an async function in the final output for await to work. And that function must be run asynchronously (eg. with async-await) etc...

So I might do the compiler option. But don't expect it too soon. It is a lot of work to do it properly.

@98mux
Copy link
Contributor

98mux commented Apr 12, 2021

@joas8211 Sorry for my noobness, could it be possible to add syntax for async instead of a compiler option?
Like this:

<script async>

</script>

@joas8211
Copy link
Author

@filipot Probably not possible. Problem is with 1: the difference of how the components are initialized and with 2: mixing the different types of components.

1: Asynchronous component is initialized with asynchronous static function on components class const instance = await Component.init(options); and synchronous aka. "normal" component is initialized with the class constructor const instance = new Component(options);.

2: Synchronous components cannot contain asynchronous components since they must function synchronously.

@probablykasper
Copy link

probablykasper commented Apr 13, 2021

@joas8211 Does the initialization of an async component have to be async? Would something like this be viable:

<script>
  data = await import('module.js')
  export let name = ''
  function handler() {
    data.name = 'updated'
  }
</script>

<p on:click={handler>{name}</p>

transforms to

<script>
  data = await import('module.js')
  export let name = ''
  async function handler() {
    data = await data
    data.name = 'updated'
  }
</script>

{#await}
{:then data}
  <p on:click={handler}>{data.name}</p>
{/await}

@joas8211
Copy link
Author

@probablykasper For my use case, yes. I wanted to load dynamic components defined by component's prop or eg. external resource before rendering on server-side / build-time (SSR). SSR only runs the initial script (top-level of script tag) and does not wait for await keyword in the template. Here's some code I just made up to demonstrate my use case:

<!-- ContentArea.svelte -->
<script>
    export let id = 'main';
    
    const response = await fetch(`/areas/${id}`);
    const blocks = await response.json();

    for (const block of blocks) {
        block.component = (await import(block.module)).default;
    }
</script>

{#each block as blocks}
    <svelte:component this={block.component} {...block.props} />
{/each}

@probablykasper
Copy link

@joas8211 Is the only issue that SSR wouldn't support it? Does SSR even support loading dynamic components currently?

@joas8211
Copy link
Author

@probablykasper Well, it does support dynamic components with static import, but not with dynamic import aka. code splitting if you use Rollup. Because dynamic import is asynchronous.

@probablykasper
Copy link

@joas8211 In that case I think it might be fair to consider special SSR handling of await as a separate issue, and then SSR could handle await like a normal promise for now. Besides, what would SSR do if the response you get in your fetch depends on a cookie, user agent or something like that?

@tonprince
Copy link

@probablykasper For my use case, yes. I wanted to load dynamic components defined by component's prop or eg. external resource before rendering on server-side / build-time (SSR). SSR only runs the initial script (top-level of script tag) and does not wait for await keyword in the template. Here's some code I just made up to demonstrate my use case:

<!-- ContentArea.svelte -->
<script>
    export let id = 'main';
    
    const response = await fetch(`/areas/${id}`);
    const blocks = await response.json();

    for (const block of blocks) {
        block.component = (await import(block.module)).default;
    }
</script>

{#each block as blocks}
    <svelte:component this={block.component} {...block.props} />
{/each}

I have the same issue and wanted to load dynamic components by an external list from sveltekit-load. In dev-mode everything is fine but when trying to load it in preview mode the components are not rendered.

@joas8211
Copy link
Author

I gave up trying to make Svelte asynchronous and preserving all the existing features. I tried to make my own fork dropping all not supported features, but tests showed me how it's really hard to make stable. So I switch framework to Crank.js for my project. It's very different from Svelte, but it ticks all my requirements except reactivity which I can solve.

@98mux
Copy link
Contributor

98mux commented Jul 11, 2021

@joas8211 You might want to checkout solidjs if you like jsx. Don't know if it has async tho

@joas8211
Copy link
Author

@filipot Thank you for suggestion, but it seems Solid doesn't support asynchronous components / rendering.

@frederikhors
Copy link

asynchronous components / rendering.

What do you EXACTLY mean, @joas8211?

@joas8211
Copy link
Author

@frederikhors I mean an ability to halt rendering for the duration of an asynchronous task. Like for example loading data or subcomponents. Suspense type of solutions won't cut it for server-side rendering.

@frederikhors
Copy link

@joas8211
Copy link
Author

@frederikhors It seems your feature request #5017 has not been acted on. If it would be implemented then I would not have this issue. I wrote an example of my use-case in a previous comment: #5501 (comment)

@98mux
Copy link
Contributor

98mux commented Jul 15, 2021

@joas8211 sapper and svelte-kit allows you to load data before you render the page https://kit.svelte.dev/docs#loading

@ryansolid
Copy link

ryansolid commented Sep 9, 2021

@filipot Thank you for suggestion, but it seems Solid doesn't support asynchronous components / rendering.

To clarify Solid supports async. It is granular though. Halting at a component level makes no sense for Solid. Cranks approach while interesting is too blocking for our needs. Having components halt reduces our ability to parallelize work within a single component and potential unrelated sub trees.

We use suspense on the server to do Async SSR and support out-of-order streaming where we send placeholders out synchronously and then load in the content as it completes over the stream.

The way we accomplish this is through the Resource API which handles automatic serialization of data and basically completes a promise in the browser that started on the server. Suspense boundaries know how to handle this by design and in so you get a universal system that just works.

@doomnoodles
Copy link

This feature would be incredibly useful to enable the use of wasm by svelte modules, since wasm init functions are async.
See proof of concept using sveltekit pages: https://github.com/cfac45/sveltekit-rust-ssr-template

@kryptus36
Copy link

I am trying to do the same thing as @joas8211 : I have a json template that describes which components are rendered and their content.

It works perfectly in CSR but in SSR there's seemingly no way to dynamically load components.

{#await appImport(widgetPath) then component}
  <svelte:component this={component.default} bind:data={data.data} />
{/await}

@DeepDoge
Copy link

It has been 2 years, React is implementing this now
because they realized web is async

Basically it should be like: Component <script> runs async and awaited before render, and being awaited all the way up to the Root
that way we can do async tasks such as fetching on SSR. for example we can fetch data from api and render result on SSR.
and if we want to fetch stuff, do async stuff on CSR we can use onMount() for that

@kryptus36
Copy link

I thought of this and #958 (which is from 2017) when I saw the React announcement. It would be nice to know if this is on the radar of the dev team. Sveltekit doesn't fit my needs, and as such I may be faced with using something else.

@bomzj
Copy link

bomzj commented Dec 25, 2022

Vue 3 supports top level await. When Svelte gets this mega convenient feature?

@axerivant
Copy link

Just saying that I think this will improve DX quite a lot.

@joas8211
Copy link
Author

If I remember correctly I used async-await through-out Svelte codebase to allow await at top-level of component script. That wouldn't be possibe without major API changes because currently the execution starts from component's constructor (https://svelte.dev/docs#run-time-client-side-component-api-creating-a-component). Constructors in JavaScript are always synchronous. So if there's so willingness for breaking changes (eg. new major version), then component top-level await cannot be achieved.

@akvadrako
Copy link

You could do it with an explicit <script async> tag, which becomes a promise that's called from the constructor. And defer rendering until it returns.

@joas8211
Copy link
Author

@akvadrako The problem is how the function execution is expected to return. I incorrectly linked the client-side component API, but the problem really is the server side API. Client-side execution continues after calling the constructor until Component.$destroy() has taken effect. But in the server-side you call Component.render() that is expected to return HTML and CSS for that component synchronously. Aka. it returns them directly instead of returning a Promise. If at any point of the rendering component is allowed to run asynchronous code, the syncronous Component.render() API won't work. And don't even ask how web components are supposed to work afterwards 😞

@kryptus36
Copy link

Given that Rich himself posted #653 and superseded it with #958 he's aware of the utility of it. Given that react took the effort on (and it took them years to achieve) and Vue 3 included it, I think there's a general consensus that it's useful. Without it you need to use tools like Puppeteer to pre-render for SEO for some render flows, a practice that was once recommended by Google, but is now seen as less favorable. All competing frameworks now have a similar function, and it makes some ideas much more difficult (or impossible) to achieve when page structure is dynamic.

It's not really a question of whether it should be done (you'd have a hard time arguing the contrary imo) but whether it's going to be made a priority. I am holding out hope, because to me this is the only thing Svelte doesn't do head and shoulders better than it's peers. The problem is this is a nearly non-optional feature for some projects.

Without this, your SSR will stop at any call that requires a dynamic import, which seriously hamstrings the usefulness of svelte:component among other things.

I'm hoping this gets put on the backlog for SvelteKit 2. It would be a big win.

@thdoan
Copy link

thdoan commented May 1, 2023

Coming from Chrome extensions development here. We are used to having top-level awaits by adding type="module". I hoped I would be able to do the same in Svelte, but that wasn't the case. It would be super cool though!

https://developer.chrome.com/docs/extensions/reference/storage/#synchronous-response-to-storage-updates

@alanxp
Copy link

alanxp commented Sep 21, 2023

Still waiting for this feature, would be a great DX

@MentalGear
Copy link

Update: Still waiting in 2024.

@DeepDoge
Copy link

As an addition to what i said earlier (#5501 (comment))
Using PageServerLoad/LayoutServerLoad or PageLoad/LayoutLoad, is also problematic, because you can't modularize things.
Only the +page or +layout can have the async load(event) function meaning you can't modularize logic into different components, you can use components only as a View. This might sound OK, at first. But then you start having problem when you wanna use same thing on multiple pages or routes.
Then you go, "I should have used a SPA only solution, or something like Astro with Web Components"

@passwordslug
Copy link

Update: Still waiting in 2024.

You mean still awaiting 🤣 (sorry couldn't resist).

@alanxp
Copy link

alanxp commented Mar 27, 2024

Svelte 5 and yet, no top level async/await

@jerrygreen
Copy link

Just to note here, each await call adds a very slightest delay because of polluting event loop. So if you don’t need async features of a library, import their sync version of a function instead.

If you use async functions just like normal functions only with await keyword, then top-level await doesn’t make sense to you. In fact, this convenience only will make you create worse apps, where now in every single component you have tons of async functions that don’t actually use async feature.

Of course there comes-in the sad reality, that libraries do not always provide synchronous versions of a function, then await comes-in handy again.

await whatever() // ❌ bad async code, you don’t actually use async feature

whateverSync() // ✅ it’s better than previous example, same result but doesn’t pollute event loop

Async makes sense only if you do something in-between:

const data = whatever()

 // do something else, probably some UI stuff etc

await data // ✅ you seem to use async mindfully, congratz 🎉

Otherwise top-level await will make you a worse programmer when it comes (if it comes). Be mindful of this convenience.

@probablykasper
Copy link

@jerrygreen That's not true, although you're right that async can make your app slower. The advantage of async is that it's non-blocking, allowing unrelated code in completely different parts of your app to run concurrently. That can make your app overall faster / more responsive if you have a long-running function.

There's no difference between your own long-running async function and an "async feature" like fetch. It's just that they realized it would suck if that was synchronous.

@txtyash
Copy link
Contributor

txtyash commented Apr 7, 2024

What should a person do to get around this problem for the time being?

@JorensM
Copy link

JorensM commented Jul 10, 2024

I need this because I have some initialization code that must run before the framework loads

@JorensM
Copy link

JorensM commented Jul 10, 2024

Update: As I needed top-level await specifically for separate modules and not the code in <script/> tags, I managed to solve this by adding the following configuration to my vite.config.js:

esbuild: {
	supported: {
		'top-level-await': true
	}
}

@DeepDoge
Copy link

So the problem here is Svelte component's themselves can't be async, best you can do is using {#await} and showing something else until promise is resolved ready.
A top-level await would allow a parent component to wait for all of its children asynchronously.

Since Svelte component's themselves can't be async, we have to use SvelteKit's PageLoad function, if we want page to not show up until everything it needs is loaded or downloaded.
But this requires you to load everything a page needs in the load function. So you can't modularize async logic into components.
So you have to structure your code based on pages instead of features. And this creates problems if you are using same thing on multiple pages.

When ever you use a component that requires an async data, you also have to remember and load it on each-page that you use that component.

for example with vanilla js, i can do something like this:

async function renderPost(postId: string) {
    const host = document.createElement("article")
    
    const post = await fetchPost(postId)
    
    const postProfile = renderPostProfile(post.profileId),
    const postContent = renderPostContent(post.content)
    
    host.append(
       await postProfile,
       renderRelativeDate(post.createdAt),
       await postContent
    )
    
    return host
}

In Svelte you can't make the Post component itself async, so you need a spinner for the PostProfile, and PostContent.
But in the example above, I can decide to have spinner at any level I want.
I can have spinners for Post itself.
If I had a Feed component is using Post component. I can decide to have spinner for the Feed and not show the Feed until all posts are loaded.

SvelteKit can also use the same system to not change the page until the +page component is resolved.

@JonathonRP
Copy link

Node has top level await support now, wonder if we can revisit svelte's top level await. Also svelte will have to eventually cross this bridge and have async components like react... And with svelte components as functions now it could be easier to make them async down the line.

@MentalGear
Copy link

I do think async components already are possible, however top-level await would be a great addition for svelte 5 !

@MrBns
Copy link

MrBns commented Jan 8, 2025

2025.. being 4 year of this issue.
can we hope for top level await in svelte ?

or is there any trick we can achieve similar behavior.

@bfanger
Copy link
Contributor

bfanger commented Jan 8, 2025

It's not an issue, it's a feature 😉
and yes there are multiple "tricks" to achieve similar behavior.

Adding top-level-await seems like it would be convenient for developers, but the implementations I could imagine are downgrades for users, it also adds a lot additional complexity and issues.

The issues

  • The promise might reject
  • The component can't be displayed until the promise is resolved.
  • The promises resolve only once

Loading & error states

As the component itself can't output anything yet, the responsibility of indicating a loading state or error state is moved to a parent component.

<svelte:boundary>
  {#snippet pending()}
    <Spinner />
  {/snippet}

  <AsyncComponent />
  <RegularComponent />
  <SometimesAsync />

  {#snippet failed(error, reset)}
    <button onclick={reset}>oops! try again</button>
  {/snippet}
</svelte:boundary>

That looks like a reasonable solution, but it's not as simple as it looks.

What if multiple blocks fail, should the failed block render multiple times, should it render at the location of the failed component? only first error or only the last?

What if an async component is displayed inside the <SometimesAsync> based on an {#if} condition that was false initially, but after an interaction became true?
Should Svelte hide the content of and show pending snippet again? But then the other components are still active, but there are no longer attached the DOM. introduce OnDetach OnAttach lifecycle events?
Debugging issues with DOM you can't see and inspect-element is not going to be fun.

What if Svelte instead used the pending snippet to replace each async component individually at their location in the DOM during the it's pending phase?

All this will increase the amount of spinners and Cumulative Layout Shifts for users.

Svelte intro & outro animations

When should intro animations start? Should intro animations wait until there are no pending components?
The RegularComponent & Spinner(s) are already setup are they not attached to the DOM, is onMount delayed?

When the Components are no longer needed and the outro animation starts, should that reject the promise?
What if the outro is canceled, how to recover from that rejection? rerun setup?

Unusable scenario's

Making the setup phase of a component asynchronous splits the setup into multiple periods (it pauses at each await), but it still runs once.

Therefor using a value from $props() to fetch data will be broken/buggy. This is because the result of the promise is only using the initial value.
Changes to that prop will not be reflected an no new fetches are made and an incorrect result will be used.

This makes it it less useful, as you need to handle the initial fetch and the updates separately.
Maybe a new rune could be introduced:

let data = $await(load(id));

That would allow re-executing the load(id) when the id signal change is detected. but what should parent component do during pending or error states?
hide the contents? show both the contents and the pending snippet?
Will the result of the server render be replaced with the pending snippet during hydration?

Tricks, workarounds & patterns

I do understand the allure of top level await, especially when React seems to be able to do it with it's React Server Component and use hook.

The {#await} block

{#if browser}
  {#await load(id)}
  <Spinner />
  {:then article}
    <h1>{article.title}</h1>
  {:catch error}
    <p>oops!</p>
  {/await}
{:else}
  <Spinner />
{/if}

The await block is very nice, if the id is state or a prop, the load(id) with re-evaluate an also takes care of race conditions.

A downside is that during server render the load(id) is evaluated but the results are ignored on the server, I'v added the {#if browser} check in the example to prevent unnecessary load on the server.
I agree with this behavior, as the await block should also be evaluated on client and showing the server result and during hydration showing a spinner again is not nice.

Another downside is that code inside the script section has no access to the result or state of the promise.

Utility to use a promise as a signal

<script>
const asyncState = createAsyncState(() => load(id));
const { data, error, pending } = $derived(asyncState);
</script>

<input type="number" bind:value={id} />

{#if pending}
  <Spinner />
{:else if data}
  <h1>{data.title}</h1>
{:else if error}
  <p>oops!</p>
{/if}

See REPL for the implementation of createAsyncState.

This also only works client-side, but the callback passed to createAsyncState is not evaluated on the server.

SvelteKit loaders

Using SvelteKit: Can the promise be determined by the url? Move the loading logic to the +page.js or +page.server.js

Splitting components

I like writing a component where all the data is always available, this makes the component reasoning about, testing and storybook easier.

The logic & complexity of loading is not gone but moved to another component or +page.js file.

Astro

Using another framework like Astro allows to use await inside the .astro components (which, just like RSC, don't support reactivity) and then use Svelte components where you need the reactivity, but I don't have much experience with this option (I like SvelteKit).

@probablykasper
Copy link

I disagree with the issues you mention:

  • The promise might reject: You can already throw errors in Svelte components.
  • The component can't be displayed until the promise is resolved: Yes, perfect.
  • The promises resolve only once: Yes, that sounds great.

Errors and loading states

It's already possible to throw errors in components today, and to load slowly (blocking). Async should ideally be treated the same way.

Svelte intro & outro animations

Intro should not wait for other pending components.
If the component is removed, it should first wait for the setup to finish.

Unusable scenario - $props()

I can't see any issue here. If you want something to re-run when $props() changes, you can use $derived().

@bfanger
Copy link
Contributor

bfanger commented Apr 29, 2025

The Svelte team has an experimental implementation of top-level await: #15845

This is client-side only (for now) and chose the following sensible behaviors:

  • must be inside a boundary
  • SSR renders the pending snippet
  • top-level await for static resources
  • $derived(await ..) for dynamic resources (based on dependencies like $props values)
  • once the data was resolved new async requests don’t trigger the pending snippet or block unrelated updates

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

No branches or pull requests