Asynchronous Svelte #15845
Replies: 22 comments 83 replies
-
so exciting!! |
Beta Was this translation helpful? Give feedback.
-
Love it's a discussion introduction for such an awaited feature! |
Beta Was this translation helpful? Give feedback.
-
This is really cool! I've been observing the
|
Beta Was this translation helpful? Give feedback.
-
When I read the document I was filled with excitement, fear and confusion |
Beta Was this translation helpful? Give feedback.
-
Not sure if I missed something, but -- there is some error handling in this demo: https://svelte.dev/playground/24eec2a85770402eae633d06aec412a6?version=branch-async but, how do you do error handling generically for any await'd thing? for example, in this parallel-promise template: <li>apples {await getPrice('apple')} each</li>
<li>bananas {await getPrice('banana')} each</li>
<li>canteloupes {await getPrice('canteloupe')} each</li> what do I do if I want to render errors if |
Beta Was this translation helpful? Give feedback.
-
Solid user here. Never used const data = createAsync(() => promiseFn(args)); |
Beta Was this translation helpful? Give feedback.
-
We have a neat little super-lightweight RPC (lighter than tRPC) setup that
This basically turns anything that we put into the API object into an async function callable from the client, with the same signature as the original server-side function. Also, since we're working with plain functions, things can be trivially wrapped with HOF et al both on the server and the client to implement concerns like authentication, validation, caching, etc. This has been really fun to use, and I'm happy to share more if there's interest in this. |
Beta Was this translation helpful? Give feedback.
-
Ah, so no freezing states in child components too, I suppose? I was hoping this was the case, it would certainly help with something like mobile stack navigations, carousels, accordions or drawers where you do want a portion of your component tree to be frozen (see svelte-freeze for the general idea, and svelte-stakkr for a router building on that idea) The entirety of svelte-freeze could be simplified into this single component, and most importantly removes the need for willing packages to opt-in to the freezing functionality. <script lang="ts">
import { untrack, type Snippet } from 'svelte';
interface Props {
frozen: boolean;
children: Snippet;
}
const { frozen, children }: Props = $props();
const PROMISE = new Promise(() => {});
</script>
<svelte:boundary>
{#if frozen}
{await PROMISE}
{/if}
{@render children()}
{#snippet pending()}
<!-- empty -->
{/snippet}
</svelte:boundary> I would admit that it's going to be a lot more complicated behind the scenes though, since now you need to have a way to have a derivation that runs outside of that suspension, so that you can check for It's certainly something that can be better handled by a different mechanism for sure, just like how React's |
Beta Was this translation helpful? Give feedback.
-
I don't understand this, I personally dislike $derived and $derived.by not working identically. Maybe someone could explain to me like I'm a 5 year old? |
Beta Was this translation helpful? Give feedback.
-
I wonder if it could be possible to somehow abort promises that are no longer needed, so that there aren't unneeded in-flight requests due to network lag As I understand, once "Overlapping updates that touch the same state must be applied in linear order" is implemented the following would happen:
So instead by aborting the older fetches the developer explicitly says "I don't care about this anymore, just give the user the latest value" Something like this maybe? export function getPrice(produce) {
return await fetch("/api/price/"+produce, {
signal: $effect.abortSignal()
})
} |
Beta Was this translation helpful? Give feedback.
-
Regarding async derived dependencies effects to be blocked until async derived resolves - you mention inputs being the exception. What about cases like a custom inputs or search bar with a clear button - pressing clear should clear that input and initiate an async search with empty value(it returns unfiltered entries then). There would be no focused input here so input will be cleared only after search resolves? |
Beta Was this translation helpful? Give feedback.
-
https://svelte.dev/playground/767e0bfd75614664a41ba73f28cc424c?version=branch-async Is this a case of 'Overlapping updates' stuff that is yet to be resolved? Or is this a different bug? I'm confused why |
Beta Was this translation helpful? Give feedback.
-
I know async SSR was mentions as a possible future development. In its current state are the component level await started on the server? or do they begin after hydration on the client? i suppose hydration will likely be much faster if it hits a pending early enough, but it still seems like a pretty big gotcha if request don't begin until after hydration. |
Beta Was this translation helpful? Give feedback.
-
This is great 💪 |
Beta Was this translation helpful? Give feedback.
-
More of an SSR question: has there been any more research or thought into how streamed components with errors will be handled? I remember reading another thread that was talking about including a meta tag if a component had an error so the page wouldn't get indexed. Interested to see where Svelte ends up. |
Beta Was this translation helpful? Give feedback.
-
In our SvelteKit apps we've heavily utilized load functions that return promises, and then use the
This works great in some circumstances. For example, you have a large data table. If the user applies a filter, changes the sort order, etc, then we call the same GET request, that returns a promise. We then assign that promise to the same variable (usersPromise in the example below) that the await was referencing. While the promise resolves the user sees the loading state, and then eventually the data loads.
This pattern doesn't quite work as nicely in other scenarios. Since the await/then requires a redraw, the UI has a brief flicker when the data updates. For promises that take a bit to resolve you don't really notice, because you get a loading state, but for promises that resolve very quickly, you don't really see a loading state and instead you just see a flicker. For example, lets say you have a list of items and the user takes an action that adds a new item. You can either make an optimistic update, or you query the backend again after the update, and get the mentioned flicker effect. While it's not too bad of an experience, it would be nice to avoid the redraw. (Using the optimistic update also requires deviating from the pattern mentioned above, which leads to less/consistent code.) What I'm wondering here is if this new syntax helps with the scenario mentioned above. I actually really like the existing await template syntax, minus the flicker effect that you get for promises that resolve quickly. |
Beta Was this translation helpful? Give feedback.
-
It would be cool if |
Beta Was this translation helpful? Give feedback.
-
I'm excited! |
Beta Was this translation helpful? Give feedback.
-
This may be a highly addictive feature, congrats to the team. The examples in the post are unbelievably clean, hard to picture how front-end code can get any cleaner. I still don't know if
Finding the right design for this seems very challenging, as the best design seems to be no design at all. Telefunc looks like a great basis anyway. I suppose this also means better-class support for GraphQL, would love to hear @AlecAivazis thoughts on this. Does this mean the Deeply excited about the direction of this project |
Beta Was this translation helpful? Give feedback.
-
Love this so much except async derives. It would be nice to keep the goal of derive to just compute values based on other states, without side-effects etc |
Beta Was this translation helpful? Give feedback.
-
Why the decision to allow async only at a boundary with a pending snippet? |
Beta Was this translation helpful? Give feedback.
-
I assume it’s the allure of an API along the lines of Telefunc. The dream
being just able to call a remote function without really even knowing or
fiddling about with designing an api etc.
…On Sun, 4 May 2025 at 12:52, Julius Berger ***@***.***> wrote:
@hmans <https://github.com/hmans> we're exploring the RPC space (
@dummdidumm <https://github.com/dummdidumm> in particular is deep in this
rabbit hole at present), would love to see what you've cooked up. I'm not
personally a fan of the tRPC approach, we've been more inspired by
Telefunc <https://telefunc.com/>, though it doesn't fully meet our needs
Could you maybe share why you're not a fan of tRPC here?
—
Reply to this email directly, view it on GitHub
<#15845 (reply in thread)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAAGNFLGQXVFFYHIVPQRAD24XWOZAVCNFSM6AAAAAB4BQKD3CVHI2DSMVQWIX3LMV43URDJONRXK43TNFXW4Q3PNVWWK3TUHMYTGMBSHE2DQNY>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
This is a long document; get yourself a cup of tea.
tl;dr
You can now use the
await
keyword in Svelte — in your<script>
, inside$derived
expressions, and in your markup — by installing theasync
branch......and adding the
experimental.async
option to yoursvelte.config.js
(or wherever you configure Svelte):You can also try things out in the async playground.
You will find bugs! This is in no way production-ready. The PR that accompanies this discussion is at #15844.
Background
In olden times, we did asynchronous work like fetching data inside
onMount
or an{#await ...}
block. This works but it's quite verbose, and offers no coordination — if two components are both fetching stuff, then they will likely have independently managed error/loading states, often resulting in a janky UI with multiple spinners.Frameworks like SvelteKit (or Remix or pre-App-Router Next.js, etc) offer an alternative: instead of fetching inside the component, we do our asynchronous work outside the component, for example in
load
functions. This typically results in better user experience (we can server-render the data, preload it, and coordinate everything) but it too has problems — prop-drilling, type shenanigans, coarse-grained invalidation, and logic that's hard to delete because it's often not obvious whether something in yourload
function is even being used.In recent years component frameworks have explored a third way: putting asynchronous work back inside components, but in a way that is coordinated:
<Suspense>
,startTransition
,useTransition
,use
and React Server Components, that together allow you to manage async updatescreateResource
API that can be used with<Suspense>
andstartTransition
/useTransition
for the same purposeawait
inside a<script setup>
in a component inside a<Suspense>
boundary, though this experimental API only works for component creation, not subsequent updatesWe believe Svelte's compiler-centric nature offers us a way to have component-level async work with significantly better ergonomics and fewer drawbacks than existing approaches.
Requirements
await
keyword. Avoid framework-specific APIs and idioms to the extent possible<script>
, inside$derived
expressions, in the template, in attributes and component props, in control flow like{#if ...}
blocks, or wherever else)await
expressions need not result in sequential async work. Similarly, asynchronous work in two sibling components (for example) should happen simultaneouslyfoo
changes, andawait bar(foo)
is (or will be) visible on the page, any occurrences offoo
in the UI should not update untilbar(foo)
resolvesWe also want this to have minimal impact on performance and memory for existing codebases, and to cause little or no breakage for existing apps.
Design
In a nutshell: you can now use
await
in three places that will today result in a syntax error:<script>
$derived
expressionWe also introduce a
pending
snippet to<svelte:boundary>
, which allows you to provide UI whenawait
expressions inside the boundary are first resolving. For now,await
expressions must be inside a boundary with a pending snippet (however deeply nested — for example you might have a single boundary at the very root of your app), though this constraint will likely be relaxed in future.If state is read inside a
$derived
or template expression with anawait
, changes to that state will not be reflected in the UI until the expression resolves. For example in a situation like this......the
<h1>
will not update until the<p>
does. (IfgetWeatherForecast
were to fail, it would activate the nearest error boundary.)To know if an update is currently pending, you can use
$effect.pending()
:If unrelated state changes while an update is pending, it will be visible immediately (assuming it doesn't separately cause asynchronous updates).
Use cases
The most obvious use case is loading data (which could be as simple as
fetch
, but in many cases will likely involve yet-to-be-designed utilities for client-server communication), but others include:<img alt="..." src={await preload('...')}>
)Avoiding waterfalls
As much as framework authors like to witter on about rendering performance, the thing that really slows apps down is latency. In some cases you can reduce latency by speeding up your back end, or prefetching content you expect to need, or serving content from somewhere close to the user, but those things aren't always possible. What you can do is mitigate the effect of latency by minimising the number of round trips between the browser and the server.
Svelte helps by doing as much work as possible in parallel. For example here...
...the three calls to
getPrice
will happen simultaneously, even though they appear in sequence. Similarly, the three wise monkeys will do all their async work together:Not all work can be parallelized. Any
await
expressions inside the<script>
run before expressions in the template, and in the sequence you would expect......though note that if
x
subsequently updates,a
andb
will be recomputed in parallel. You can of course avoid the initial waterfall like so:A brief digression on 'necessary' waterfalls and server components
While unnecessary waterfalls can generally be prevented by pushing async work as far into the 'leaves' of your application as possible, there are also necessary waterfalls to contend with. For example, you can't use
Promise.all
here, because you need to knowartist.id
before you can callgetTrackListing
:It's better if you can do both fetches on the server, near your database. There are two ways to solve this: either anticipate the need for the track listing when you do the search...
...or use a mechanism like React Server Components, where the rendering happens on the server. Option 1 has less-than-ideal ergonomics, though it's basically equivalent to SvelteKit's
load
function. Option 2 is optimal from a waterfall prevention perspective, but we feel RSCs have significant trade-offs.We anticipate that opinionated data-fetching patterns will emerge over time, along with new approaches inspired by RSCs, to solve this problem.
Future work
The work presented so far is the first step of a multi-stage process. While it's useful in its current form, more value will be unlocked with later stages:
Async SSR
Today, server-side rendering is fully synchronous. Because of this, if a
<svelte:boundary>
with apending
snippet is encountered during SSR, thepending
snippet will be rendered.It would be better to render the content instead, but this requires that SSR become an asynchronous operation.
In an ideal world we would also be able to stream the result where appropriate. This is complicated by the fact that you ideally need to know what's in the
<head>
before you get to the<body>
. We have some ideas for how to design this sensibly, but it's not a current priority.Once we get to this part, it's likely that the requirement for
await
expressions to be contained in a<svelte:boundary>
will go away.Forking
In SvelteKit we preload the code and data necessary for a navigation before it even occurs: when you hover over a link, or tap it, we import any modules needed by the route and begin running whichever
load
functions need to run. (This behaviour is configurable, of course.)To make this work in a world where asynchronous work happens inside components, we need to be able to pretend that a state change occurred, and do the resulting updates 'off-screen'. The result can either be applied or discarded.
Colloquially, we've been describing this as a 'fork'. You could imagine several unresolved forks coexisting simultaneously until one reality is chosen (by the user clicking on one of several links they've been close to interacting with, for example). Most likely, we'll add an API to Svelte that allows frameworks like SvelteKit (but also particularly ambitious application authors) to enter the multiverse.
This all sounds very complicated, but in reality it's not all that different to how asynchronous updates already work. Nevertheless, it will take some finagling to get right, and in the interests of shipping we haven't included this work in the current PR.
Client-server utilities
Unless we want to create server endpoints for everything we want to
fetch
inside a component, we'll need tools for interacting with the server. Those tools will need to consider security, HTTP caching, mutations and invalidation, optimistic UI, batching, streaming, custom serialization, type safety and lots of other things.We're at the early stages of figuring out what this all looks like, but we're very confident that the foundation we're laying here will allow us to design something really good.
Improving SvelteKit
Anticipating everyone's reaction: no, we're not going to suddenly make you rewrite all your SvelteKit apps. But taking all the other stuff together, a picture starts to emerge: a version of SvelteKit that's a thinner layer on top of Svelte. For example, as proud as we are of our zero-effort type safety, it's something you no longer need if you're fetching data directly inside your component.
Meanwhile the framework internals could potentially get simpler, because we'd be able to rely on common primitives inside Svelte itself. (Today, we can't reuse that much code between the server and client portions of SvelteKit, and we rely a lot on virtual modules and generated code. Some of that is for historical reasons, but some is necessary because anything asynchronous has to happen outside Svelte.)
It seems likely that a lot of interesting new possibilities will open up as a result of this work, and it seems at least plausible that we'll collectively find ourselves drifting away from primitives like
load
. Exactly what this would look like is to be determined, and it's something that we'll figure out together as a community.Nuances
No
startTransition
/useTransition
If you've used React or Solid's suspense implementation, you will have encountered
startTransition
anduseTransition
. These functions allows you to update state in such a way that the nearest suspense boundary doesn't show its fallback while waiting for any async work that is downstream of the state change.In Svelte, we don't do this. Instead, state changes that result in asynchronous work always have their effects deferred until the asynchronous work is complete. While that work is ongoing,
$effect.pending()
istrue
. Anypending
snippets are only shown when a boundary is first being created.This does create the possibility that something distantly connected to a given piece of state has the power to delay (or prevent!) changes to that state from being reflected in the UI. We think this is preferable to the alternative (in which updates outside a
useTransition
or similar cause unwanted fallback UI to appear), though it may be necessary to develop techniques for identifying these chains.Overlapping updates that touch the same state must be applied in linear order
In a case like this...
...it's possible to imagine a scenario in which a change to
a
is followed by a change tob
while the firstfetch
is ongoing. If the secondfetch
somehow finishes first, we don't want the firstfetch
to be applied afterwards, since the resulting UI would show stale values.As such, a given async expression must only resolve after its previous versions have also resolved.
Async deriveds are eager
Normally in Svelte,
$derived
expressions use 'push-pull' reactivity — they are invalidated when their dependencies change (the 'push') but are not re-evaluated until something reads them (the 'pull'). This doesn't work for async deriveds, because if we waited until the value was read the resulting promise would never resolve in time.Instead, an 'async derived' is really just an effect and a source signal in a trenchcoat. We evalute the expression in the effect, and when the promise resolves, set the source value (unless the effect fired again in the interim).
For the most part, you don't need to think about this. There is one important implication though — while it's possible to create an 'unowned derived' (in other words, a derived that is not part of the 'effect tree', such as one created in an event handler) it is not possible to create an unowned async derived, because effects can only be created inside other effects. (Don't worry if you didn't follow this — it's inside baseball stuff, and describes an edge case you're unlikely to encounter. Svelte will tell you if you get this wrong.)
Context is preserved in expressions
When a reaction (i.e. an effect or derived) runs, we track its dependencies by seeing which values are read during its execution. In pseudo-code:
(This is simplified and wrong, but you get the idea.)
For asynchronous functions, this presents a challenge. To understand why, consider the order in which things happen here:
This will log
a 1
,b 1
,a 2
,b 2
— in other words despite the fact thatb
isn't even awaiting an asynchronous value,b 2
isn't logged until after it has returned toa
.In Svelte terms, this would mean that
$derived(await a + b)
or an equivalent{await a + b}
template expression would register a dependency ona
but not onb
. But since we're a compiler we have a trick up our sleeves: we wrap theawait
expression in a function that restores the effect context, so thatb
is treated as a dependency.On one level this is spooky compiler magic. But on another, it's just making the system work how a reasonable person would expect it to work. It does mean that if you extract the logic out into a function —
{await a_plus_b()}
— it will be treated differently, but during development Svelte will catch those cases and help you fix them.No async
$derived.by
While you can do this...
...
x
will then be aPromise
, not the value ofawait y
. There's no sensible place to put theawait
that would result inx
not being aPromise
. (This is why it's pretty great that we're a compiler and can make the expression form the default, even though it makes other framework authors mad.)If you have sufficiently complex logic that you need to use a function block, just declare the function and call it:
Note the aforementioned caveat around context preservation — make sure you pass state into the function, rather than having the function close over the state it references, so that dependencies are correctly attached.
Focused inputs are not overridden
Normally, state does not update in the UI until everything that depends on it has finished resolving. This does not apply to a focused
<input>
element, which in many cases will be the source of the state change in question — instead, the rest of the UI 'catches up' to the input.Breaking changes
Because of how async reactivity works, there is one small and unavoidable breaking change:
beforeUpdate
and$effect.pre
callbacks no longer run before control flow blocks are updated. If you're using those callbacks to set state that control flow logic depends on — which you absolutely shouldn't be — then you may experience breakage. As a result, we may have to wait until 6.0 for a non-experimental release of this stuff.Ok so
That was a lot; congratulations to those of you still here. I am sure you have questions, and that's a good thing. This is the place to ask them, or to offer feedback on the design. Let's go!
Beta Was this translation helpful? Give feedback.
All reactions