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

Top-level await #5501

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

Top-level await #5501

joas8211 opened this issue Oct 5, 2020 · 44 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

@bugproof
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

Vue has it too

@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
	}
}

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