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

Svelte suspense (request for comments) #3203

Open
brucou opened this issue Jul 9, 2019 · 32 comments
Open

Svelte suspense (request for comments) #3203

brucou opened this issue Jul 9, 2019 · 32 comments

Comments

@brucou
Copy link

brucou commented Jul 9, 2019

After reading this issue, I came up with a Suspense component for Svelte, replicating the behaviour of React Suspense. No React Cache, no throwing promises, no modifying your component to fit a use case, just Svelte component composition. A demo is available in the corresponding GitHub repository. Note that I could not have the demo running in Svelte REPL due to some issues with loading the axios package.

svelte suspense demo

The behaviour of the Suspense component is implemented with the Kingly state machine library. The summary of 'findings' can be found here. For info, here is the underlying state machine specifying the suspense behaviour:

suspense machine

The demo showcases the API and I will quickly illustrate it here. The demo consists of loading a gallery of images. The suspense functionality is applied twice: when fetching the remote data containing the image URLs, and then for each image which is subsequently downloaded. While the remote data is fetched, a spinner will display if fetching takes more than a configurable time. Similarly, images placeholder will also display a spinner if downloading the image takes more than a configurable time.

Firstly, the suspense functionality for the remote data fetching is implemented as follows:

<script>
 ... a bunch of imports

  const iTunesUrl = `https://itunes.apple.com/in/rss/topalbums/limit=100/json`;

  function fetchAlbums(intents){
      const {done, failed} = intents;
      axios.get(iTunesUrl)
           .then(res => res.data.feed.entry)
           .then(done)
           .catch(failed)
    }

</script>

<div class="app">
    <Header />
    <div class="albums">
        <Suspense task={fetchAlbums} let:data={albums} timeout=10>
            <div slot="fallback" class="album-img">
                <img alt="loading" src="https://media.giphy.com/media/y1ZBcOGOOtlpC/200.gif" />
            </div>
            <div slot="error" class="album-img">
              <h1>ERROR!</h1>
            </div>
            <LazyLoadContainer>
                {#if albums}
                  {#each albums as album, i}
                  <LazyLoad id="{i}">
                      <Album {album} />
                  </LazyLoad >
                  {/each}
                {/if }
            </LazyLoadContainer>
        </Suspense>
    </div>
</div>

Note that the fetch task and minimum time (timeout) before displaying the spinner is passed as parameters of the Suspense component, while the fetched data is exposed to the slot component through the data property. Note also how the fetching function is passed the done and failed callback to signal successful completion or error of the remote fetching.

The fallback slot is displayed when the timeout is expired. The error slot is displayed when fetching the data encounters an error.

Secondly, the Album component suspense functionality is implemented as follows:

<ul class="album">
    <li class="album-item">
        <Suspense let:intents={{done, failed}} timeout=0>
            <div slot="fallback" class="album-img">
                <img alt="loading" src="https://media.giphy.com/media/y1ZBcOGOOtlpC/200.gif" />
            </div>
            <a href={link} target="blank" class="link">
                <img class="album-img"
                     on:load={done}
                     src={image}
                     alt={'itunes' + Math.random()} />
            </a>
        </Suspense>
    </li>
    <li class="title album-item">
        <a href={link} target="blank" class="link">
            {title.slice(0, 20)}..</a></li>
    <li class="price album-item">Price:{price}</li>
    <li class="date album-item">Released:{formatDate(date, "MMM Do YY")}</li>
</ul>

This time the Suspense component passes done and failed callbacks to its children slots. When the image is loaded, the done callback is run.

This works well and I believe the API separates well the suspense functionality or concern from the slots. What we basically have is parent and children components communicating through events, except that the event comes included in the callback. As the demo shows, there is also no issues with nesting Suspense components.

This GitHub issues has two purposes:

  • gettign feedback on the API
  • giving feedback on Svelte slot composition

The first point is more about hearing from you guys.

About the second point:

  • slot composition is a powerful and flexible mechanism, specially in conjunction with scoped slots
  • however, a few things would be nice:
    1. being able to operate on the slot as if it were a regular html element. This mean the ability to style a slot with classes or possibly other attributes (<slot class='...' style='...'> </slot>). Add some extra attributed to cover generic needs, i.e. needs that are independent of the content of the slot. To implement the suspense functionality I had to resort to hide the default slot with display:none. Unfortunately to do that I had to wrap the slot around a div element, which can have side effects depending on the surrounding css. A syntax like <slot show={show}> </slot> would have been ideal. After thinking some more, I think that slot cannot be considered as regular HTML elements but as an abstract container for such elements. The operations allowed on slots should be operation on the container, not on the elements directly. Styling or adding classes an abstract container does not carry an obvious meaning, as the container is not a DOM abstraction. The current operations I see existing on the container are get (used internally by Svelte to get the slot content), show could be another one. The idea is that if you have a Container type, and a Element type, your container is C<E>. If you do operations that are independents of E, you can do only a few things like use E (get), ignore E (don't use the slot), repeat E (not sure how useful that would be), conditionally use E (show, of type Maybe). Using any knowledge about the E I think leads to crossing abstraction boundaries which might not be a good thing future-wise.
    2. having slots on component just like if components were regular elements
    3. having dynamic slots. In the Suspense component, I use if/then/else to pick up the slot to display, which works fine (see code below). It would be nice however to have <slot name={expression ...}>:
{#if stillLoading }
  <slot name="fallback" dispatch={next} intents={intents} ></slot>
{:else if errorOccurred }
  <slot name="error" dispatch={next} intents={intents} data={data}></slot>
{:else if done }
  <slot dispatch={next} intents={intents} data={data}></slot>
{/if}
<div class="incognito">
  <slot dispatch={next} intents={intents} ></slot>
 </div>

I am not really strong about the dynamic slots. It might add some complexity that may be best avoided for now. The first and second point however I believe are important for abstraction and composition purposes. My idea is to use Svelte components which only implement behaviour and delegate UI to their children slots (similar to Vue renderless components). Done well, with this technique you end up with logic in logic components, and the view in stateless ui elements.

The technique has additionally important testing benefits (the long read is here).

For instance the behaviour of the Suspense state machine can be tested independently of Svelte - and the browser, and with using a state machine, tests can even be automatically generated (finishing that up at the moment). Last, the state machine library can compile itself away just like Svelte does :-) (the implementation is actually using the compiled machine).

About testing stateless components, Storybook can be set to good purpose. What do you Svelte experts and non experts think? I am pretty new with Svelte by the way, so if there is any ways to do what I do better, also please let me know.

@ryansolid
Copy link

Perhaps my misunderstanding, but I understand the key benefit of Suspense to be the inversion of control (children control the loading/entering the suspense state). It isn't difficult today to conditionally render child content based on whether its loaded, to show previous state, or a loading indicator on a cancelable timer. You can always push that to the parent to achieve this effect. What makes Suspense interesting is the parent goes "Here's a placeholder. Children do whatever you need to do, I don't even need to know what exactly. I'm here to support you and make sure you don't embarrass yourselves." We can probably avoid throwing Promises, but I think we need to at minimum execute the children without showing them if necessary. This makes everything more complicated like conditional rendering in between the Suspense placeholder and the asynchronous call. But that's the problem we need to solve.Thats what makes this interesting.

@brucou
Copy link
Author

brucou commented Jul 30, 2019

@ryansolid I would agree with most of that. Suspense in React is generic in the sense that its behaviour does not depend on its children. Children indeed signal (through promise sending) something to their parent, with the parent deciphering the meaning. For now it is just a 'not ready' signal but it could be anything, this is really a communication children - parent that is occurring here. I know only three ways of doing that: shared state, events, and passing continuations (callbacks, coroutines etc.), each with its own set of tradeoff.

The <suspense> component I propose is doing the same, except that the communication child - parent happens through callbacks (done, failed) which are injected by the parent. It could also happen through events (dispatch event emitter is also injected, and done is in fact sugar for dispatch(DONE_EVENT)).

One advantage of throwing a promise is that it seems to magically remove the necessity for a communication interface, e.g. there is no need to inject anything in the child as throw comes with JavaScript just like window comes with the browser (and the swallows with the spring? I digress). On the downside, you are producing an effect on the whole program execution (modifying the call stack etc.), and well you are changing however slightly the pragmatics of exceptions. Using throw as an event emitter, and exception bubbling on the call stack is an original albeit transgressive idea but I would wait and see what consequences that brings in the future.

We can probably avoid throwing Promises, but I think we need to at minimum execute the children without showing them if necessary.

That exactly what I do. Children are wrapped in a display:none styled div and set in the template, i.e. they are executed. The issue I describe in my OP is that adding the div to hide the slot content has side effects on the styling precisely so in some cases, the children will temporarily render poorly.

Here is the displaying code:

<style>
.incognito {
  display: none;
  border: 0;
  padding: 0;
  margin: 0;
}
.cognito {
  display: inherit;
  border: 0;
  padding: 0;
  margin: 0;
}
</style>

{#if stillLoading }
  <slot name="fallback" dispatch={next} intents={intents} ></slot>
{:else if errorOccurred }
  <slot name="error" dispatch={next} intents={intents} data={data}></slot>
{:else if done }
  <slot dispatch={next} intents={intents} data={data}></slot>
{/if}
<div class="incognito">
  <slot dispatch={next} intents={intents} ></slot>
 </div>

This behaviour is illustrated in the example I gave:

<Suspense let:intents={{done, failed}} timeout=0>
            <div slot="fallback" class="album-img">
                <img alt="loading" src="https://media.giphy.com/media/y1ZBcOGOOtlpC/200.gif" />
            </div>
            <a href={link} target="blank" class="link">
                <img class="album-img"
                     on:load={done}
                     src={image}
                     alt={'itunes' + Math.random()} />
            </a>
        </Suspense>

The <a href={link} > links need to be displayed (executed) for the download of the image to start.

That brings me to the last point which is about another advantage of the Suspense I propose over React:

  • There may be a significant refactoring of components to become Suspense-ready (adding the thrown promise or adding React Cache to the equation). Taking the previous example with <a href={link} > links, there is no need to do anything else than using the public API of the <a> html tag. In other words, the difference between a suspended image link and a regular image link is minimal (on:load={done}).
  • React Suspense seems specialized to data fetching (hence React Cache). The Svelte Suspense I propose is unrelated to data fetching. It accepts a task to run when mounting. That task can be anything. It can be a memoized fetch, as in React, or it can be a sending data to a logger or whatever makes sense to your use case.

@brucou
Copy link
Author

brucou commented Jul 30, 2019

@ryansolid After rereading your message, actually I think I do not understand that part:

I understand the key benefit of Suspense to be the inversion of control (children control the loading/entering the suspense state).

In most of the suspense example I have seen, the loading state is entered immediately (fetch happening when the component is mounted, or React Cache used immediately). It is of course possible to have a suspended component which throw its promise post rendering or mounting but I wonder how that would work. Did anybody try that. I am curious.

Anyways, in the Suspense component I propose, a task if any is run on mounting the children, and the results (if any) of this task are passed to the children. A different logic is easy to implement by just changing the state machine. The whole control flow logic is embedded in the machine. In fact, if you remove the Spinning state from the machine, you have the machine for a Promise. So that Suspense machine is just Promise with a twist. You can add as many twists as you like.

@ryansolid
Copy link

It's entered immediately often (although not always), but the child is responsible for loading the data. So even if there is a cache etc, it's the execution of the child that actually does the async request etc. The Suspense element itself is above it in the hierarchy(usually a different component) and isn't aware of the specifics. So when the execution starts in the parent and you execute the Suspense element your aren't suspended, but when the downstream child is executed and does an async request that is the point at which it is realized. So from that perspective, the image loading example holds up, but the data loading one where the parent doing the fetch is too coupled I think.

The original example for React I believe was roughly the following. Pretend we have a Component on the page that has a navigation bar and a main content section. As I change the navigation the content changes and may or may not have different async data needs. On a change of navigation I want to show the existing content until the new navigation has fully loaded, and if it takes too long show a spinner.

So we implement this by wrapping the content section with our Suspense Component. Lets say after 300ms it shows a spinner until the section fully loads. So say one child loads feeds, one loads albums. We start by rendering the feeds page. It realizes that we need to do a data load and triggers Suspense. When it is complete we see the feeds content. When the user clicks "Albums" we update the navigation highlight (perhaps with a sliding animation that takes 300ms). It triggers the navigation change loading the Albums Component doing a different data load triggering the parent Suspense. However we still show the Feeds content. If it takes over 300ms we show we replace it with a spinner otherwise if less we replace the Feeds content with the Album content. At no point did we flash the page blank for 100s of ms or load an unnecessary spinner.

I think your approach could handle this. But the key is that the child have different data needs (one loads feeds, one loads albums) so the handling of that belongs with the Child Components and not with the parent container.

@brucou
Copy link
Author

brucou commented Jul 30, 2019

What you describe is the following flow:

  • navigation event => fetch action, start timeout
  • timeout expires, fetch has not resolved => render spinner
  • timeout expires, fetch has resolved => ignore
  • fetch resolves ok => render some content
  • fetch resolves nok => render fallback content
  • timeout has not expired && fetch has not resolved => DO NOT REMOVE STUFF FROM DOM

Up to the last part it is fairly easy to implement, this is the standard event-action paradigm which characterizes reactive systems. The last part however does not have the form event => action. For instance, timeout has not expired is not an event, it is a piece of state.

Instead of routing let's consider a master-detail navigation. We have the following component tree rendered at t0:

<A>
  <B>
</A>

User clicks at t1:

<A>
  <B />               // Master
  <C invisible /> // Detail
</A>

Fetch is successful:

<A>
  <B />
  <C />
</A>

In this case, we are covered because we only add to the existing hierarchy. So the DO NOT REMOVE STUFF FROM DOM part is already the normal rendering behaviour. I mean by that what is displayed by A and B is not going anywhere.

If we talk about routing, from url1:

<A />

User clicks at t1 (no rendering, but fetch is ran) and we go to url2 (or later?):

<A />

Fetch Succeed event before Timer Expired event:

<B />

Timer Expired before Fetch Succeed arrives:

<Spinner />

I confess that I have no idea what is the behaviour of Svelte with respect to routing. Outside of Svelte, there are routing libraries which have asynchronous transitions, and that could probably implement the sought out functionality. But the functionality would be coupled to the router, so probably the fetch would have to be in the router, not in the component. In the same way, the Suspense functionality is integrated into React (because Error Boundaries are integrated into React),

@ryansolid
Copy link

Yeah, I was thinking as much. Any conditional(routing) in between the Suspense placeholder and Child Component Resolution would have to be Suspense aware. If this was solvable, I think pretty much any suspense case would be. If the Suspense API here was standardized for Svelte this could be something a Router provider could build in if they could observe the same state. Mind you they would need to run multiple paths at the same time. Without switching the display necessarily.

React's versions is absolute in that even changes to the current Component would not track during that period (since they interrupt rendering) but that might be unnecessary in practice. I also wonder React's Suspense is just a restrictive implementation of a subset of the use cases here and the real feature looks a little different. Especially when put against a different system (like Svelte).

In any case I think we understand each other. I think the Routing case can probably be a future consideration. The key part for me is is that any data fetching mechanism should be triggerable from below. Separating the display handling of fallbacks from the details of what goes on below.

@brucou
Copy link
Author

brucou commented Jul 31, 2019

If you would do client-side routing by hand, the behaviour you want is described by the following machine -- note how I reused the Suspense machine, by replacing the START triggering event by the route changed event:

Imgur

@brucou
Copy link
Author

brucou commented Jul 31, 2019

Actually I am thinking that maybe this could be solved if Svelte has a special NULL value for templates. e.g. when a template evaluates to empty, then don't update the DOM (I believe the current behaviour would be to detach the relevant pieces DOM).

So if from <A/> you should render <B/> but B after computation renders the special NULL value, then no op. Otherwise, detach A, attach B. So the action DO NOT REMOVE STUFF FROM DOM can be signaled to Svelte just like any other render actions. So the flow I mentioned above would become:

  • navigation event => fetch action, start timeout, render special NULL value
  • timeout expires, fetch has not resolved => render spinner
  • timeout expires, fetch has resolved => ignore
  • fetch resolves ok => render some content
  • fetch resolves nok => render fallback content

If I dare say, that would be analog to encoding exceptions in a return value instead of throwing.

@sschottler
Copy link

sschottler commented Dec 12, 2019

the key benefit of Suspense to be the inversion of control (children control the loading/entering the suspense state)

Excellent point and it helped me think more clearly about a presentation I had to give on suspense and concurrent mode.

it's the execution of the child that actually does the async request etc.

The key part for me is is that any data fetching mechanism should be triggerable from below.

What's interesting is that the inversion of control to children does not necessarily mean the children initiate the promise they are waiting on. That was emphasized more in their additional blog post:

https://reactjs.org/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html

In the case of data fetching, they don't necessarily trigger the data fetch. They just check on the state of that request (such as calling read() on result returned by wrapPromise from the example code). Event handlers (route changes, tab clicks, button clicks, etc.) that trigger showing the view could initiate data fetches:

https://reactjs.org/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html#fetch-in-event-handlers

So how do they maintain this inversion of control then? How do parents stay agnostic of children data dependencies to maintain loose-coupling/maintainability/etc.?

In the case of Relay and GraphQL, the components declare the "fragments" of data they need and then Relay analyzes them at build time and aggregates the fragments into top level queries that are kicked off on routing changes, etc. The initiating of the data fetch promise(s) is built into the router.

So children's data dependencies are still colocated with the component, but not necessarily the fetching of that data.

They also have a REST proof of concept:

https://github.com/gaearon/suspense-experimental-github-demo

In that example, the components have separate data files that allow them to colocate data dependencies. However, the top level aggregated parallel fetch on route change is not generated at build time and is instead written by hand. Example:

https://github.com/gaearon/suspense-experimental-github-demo/blob/master/src/UserPage.data.js

But in theory, they could be generated as well. If not, then you lose some of that loosely-coupled inversion of control. But if you generate them at build time like the Relay case, maybe not?

We can probably avoid throwing Promises, but I think we need to at minimum execute the children without showing them if necessary. This makes everything more complicated like conditional rendering in between the Suspense placeholder and the asynchronous call.

I haven't gone through the relay docs and example app yet to fully understand how it works. I'm also curious about things like conditionally rendered components inside suspense boundaries and how that would work with these generated aggregated queries for a tree of components.

@ryansolid
Copy link

What's interesting is that the inversion of control to children does not necessarily mean the children initiate the promise they are waiting on.

Yeah I did not understand that at the time but they definitely do not need to do the async request themselves just trigger Suspense on read. This wasn't as clear before they released the more recent docs for data-loading.


Since last posting to this conversation, I've worked through designing a Suspense pattern that can be mimicked with a reactive library and I've implemented them in my reactive UI library(SolidJS).

The first piece comes down to understanding how rendering works in Svelte, as rendering has to be non-blocking. We need to ensure that Components are still able to go through initialization code without being visible. In Solid's case I do it before attaching to the DOM, but if we need to mount with Svelte it can still work by controlling visibility.

The next is having a means to setup reading of a Suspense state. I ended up using a Context API mechanism to do that making the Suspense Component essentially a Provider. From there specialized loading methods can read into that context to maintain suspense state. I am unsure of an easy way to handle useTransition equivalent with Svelte since it doesn't really match the DX experience, but there is probably another way to configure the timeout.

Finally all control flow directives in Suspense boundaries need to be aware of this same context. From there it is fairly easy to hold changes. It might make sense for this behavior to be opt in (some syntax addition) to allow better tree shaking since Suspense boundaries can be cross Component meaning that the compiler probably will have difficulty predicting this completely from the compilation step.

I assume the Svelte solution would need to be a little different but the fundamentals should be similar. If its any help the latter half of my medium article might shed some clues I'm not thinking of at the moment. This conversation helped me take the first steps along this path and I'd love to help any way I can.

@brucou
Copy link
Author

brucou commented Dec 12, 2019

@sschottler While I understand the general principles behind React Concurrent Mode, I am not sure I understand in the details how it works. Splitting work into chunks, and giving back the hand to the browser for user event handling obviously improves the reactivity of the application. What I don't understand is how the back pressure issues that it creates are solved. Conceptually this is similar to a search-as-you-type field. Any field update triggers a new query, and then responses come at any time and in any order. So if you want to display responses in-order, in the worse case (the response for the first query comes last), you have to buffer as many responses as there were queries. In the real world, you would drop the responses that do not correspond to the current query - which correspond to lossy back-pressure.

The analogy with Concurrent Mode is that user interactions may generate new render queries. It is not clear to me whether React discards the vDOM it was computing (no longer current) to focus on the new vDom triggered by user interaction; or do both vDOM computation in parallel, and displays (commit phase) them individually in order when computation is finished; or display what it has computed so far on every requestAnimationFrame; or does a merge between vDOM version a-la git; or ... But that gets up away from the OP's topic.

Indepedently of the specific techniques used, I think the API for Suspense is worth meditating on. As I said in a previous comment, some form of communication children-parent will have to be involved, and I can think of three ways to do that: shared state, events, and callbacks. So @ryansolid seems to be using shared state (context). I used callbacks in my previous Svelte examples (callback which use events under the hood). The point is while the technique may differ, it will be possible to implement suspending functionalities no matter what the framework/library is. The issue for me is thus the API. My preferred component organization is to have component with a single concern. That means presentational component, render-less or logic component, layout components, etc. Suspense enters in the category of the logic component and ideally its children should be presentational component.

Now, lets review the great examples of @ryansolid:

  1. Fallback Fundamentals
    <Suspense fallback={<Loader />}>
      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <LazyChild start={startTime} />
      <LazyAsyncChild start={startTime} />
    </Suspense>

This is a good example, because the suspense logic resides in the Suspense component. However there is not a complete separation achieved here because the code for the children components would change (however little) if no suspense functionality was used, e.g.

      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <AsyncChild start={startTime} />
      <LazyChild start={startTime} />
      <LazyAsyncChild start={startTime} />

But on the surface of things, it looks good enough.

  1. Transitions
        <Suspense fallback={<Loader />}>
          <Switch transform={awaitSuspense}>
            <Match when={state.tab === 0}>
              <AsyncChild page="Uno" />
            </Match>
            <Match when={state.tab === 1}>
              <AsyncChild page="Dos" />
            </Match>
            <Match when={state.tab === 2}>
              <AsyncChild page="Tres" />
            </Match>
          </Switch>
        </Suspense>

This is also a terrific example, where Suspense does the suspensing, Switch and Match do the tab switching, and AsyncChild is concerned with presentation and asynchronous data fetching.

There again if I wanted to discard the suspense implementation detail, I can just remove the Suspense component (almost).

  1. Suspense List
const App = () => {
  const resource = fetchProfileData(0);
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <ProfilePage resource={resource} />
    </Suspense>
  );
};

const ProfilePage = ({ resource }) => (
  <SuspenseList revealOrder="forwards" tail="collapsed">
    <ProfileDetails user={resource.user.value} />
    <Suspense fallback={<h2>Loading posts...</h2>}>
      <ProfileTimeline posts={resource.posts.value || []} />
    </Suspense>
    <Suspense fallback={<h2>Loading fun facts...</h2>}>
      <ProfileTrivia trivia={resource.trivia.value || []} />
    </Suspense>
  </SuspenseList>
);

Same comments, the difference is that the fetch is the at the top level App.

So the questions for me here are two:

  • how to remove completely the child component awareness of dealing with async data? i.e. not having resources and resource.value stuff in the child component code, but just directly the value. That would mean that child components would be ideally dumb presentational components. I think the Suspense List example is the closest to that: ProfileTimeline is a dumb component.
  • how to do this within svelte mechanism

@ryansolid
Copy link

Ok first on Concurrent Mode. React uses their own scheduler but essentially it's a play on requestIdleCallback. I implemented the Sierpinski demo and basically made my own version of this scheduling. The key to it is to use deadline based scheduling. requestIdleCallback has 2 really useful things that aren't as well known. First the second optional argument of timeout is key to ensure that at worst the call becomes a setTimeout. Second requestIdleCallback calls your callback with an object with 2 really useful things didTimeout and timeRemaining. Using these you can push work on to a queue and while time remains (browser based heuristic estimate of available idle time) you can pull work off the queue and process it in a loop. If you set a reasonably fast timeout and figure out default behavior like say pulling off atleast one item in the queue each time you can basically schedule all work off just repeatedly requestingIdleCallback. I mean that is just tip of the iceberg if you can estimate work effort you could probably do even better scheduling.

This repo helped me understand the potential of different scheduling mechanisms: https://github.com/justinfagnani/queue-scheduler

That being said this is no silver bullet and has tiny bit of overhead on normal operation even when able to use the idle time as now all updates are asynchronous. I implemented this mechanism in a demo by essentially by passing in reactive var and creating a new reactive var that basically acted like a buffer. Then I'd queue up reading the first reactive variable to set the 2nd in a computation if it wasn't already scheduled. It's a cool demo but I think it's really inconsistent thing to generalize. Quite often skipping this is just so much faster it doesn't matter. Without having to deal with Top Down reconciliation in a reactive library the gains aren't often as big. I see some uses for introducing this sort of scheduling but I think it probably needs to be opt in or deliberate unless you want performance across the board to take a bit of a hit. Sort of a self fulfilling prophecy if not done perfectly and I think that is an area that needs way more research.

But luckily for a reactive library we can do Suspense without Concurrent Mode since we already have that granularity at our disposal. To your questions. I'm not sure you can always make the child unaware. I set up solid so that the Suspense aware helpers like loadResource can work without Suspense since it's just a Context lookup that on failing to find can just skip. But if the child does loading they are aware. In React the children need to be aware even on read since they need to get passed down the resource and then read from it. The reason I don't in Solid is since every binding expression is wrapped in a function and then not ran until the property is accessed they can treat the read like any other prop access. Basically all props are lazy evaluated. This is a runtime trick. And honestly a happy accident, I wasn't aiming for it but once I wrote the example I had that moment that I realized how powerful that is.

But can you do similar with Svelte's compiler/reactive system? Probably, but not 100% sure. Even React requires the child component to be aware they are reading from a resource (special object). Svelte does have reactive props that can be passed down so if there is some way to hide it in there I believe it can work the same way.

@sschottler
Copy link

I see some uses for introducing this sort of scheduling but I think it probably needs to be opt in or deliberate unless you want performance across the board to take a bit of a hit.

I asked Kent C Dodds about this:

kentcdodds/ama#766

His post made me think some of this scheduling happens automatically, but in my simple experiments, I've had to opt in by wrapping my state changes in startTransition or using the useDeferred hook. I'm curious if React has heuristics for knowing to automatically schedule some things, but if it does, they weren't triggered by my simple tests.

@ryansolid
Copy link

Yeah I wonder too. They've built a queuing system if you look at the source code that they could apply anywhere. They basically polyfill requestIdleCallback by using the timestamp on requestAnimationFrame and couple other tricks to determine available time. But from there it works the same way. Their scheduler takes a priority argument and then prioritizes appropriately. So I imagine it's a question of whether they decide internally to apply this scheduling to certain types of actions. I forget where now, but I saw discussions where they were talking about how they'd classify priority of certain types of actions generally so they could be implementing that already. And if it's things like state updates are triggered from the network it might not be that noticeable. However useDeferredValue feels a lot like this as well.

@sschottler
Copy link

sschottler commented Dec 14, 2019

React core team member responded regarding whether concurrent mode requires opt-in:

https://twitter.com/acdlite/status/1205694631398137857?s=20

@antony
Copy link
Member

antony commented Apr 9, 2020

Is it worth turning this into a RFC (https://github.com/sveltejs/rfcs)?

@WaltzingPenguin
Copy link

WaltzingPenguin commented Mar 12, 2021

Either I am dramatically misunderstanding the problem, or this conversation has gotten quite sidetracked from the original goal.

It looks like we can do a very good approximation of Suspense with existing infrastructure.

This lets us write code like:

// parent.svelte
<Suspense>
  <Loading slot="loading" />

  <Child />
  <Child />
</Suspense>

// child.svelte
<script>
import { createSuspense } from 'suspense'
const suspend = createSuspense()

const data = fetch('/something')
</script>

{#await suspend(data) then item}
 { item }
{/await}

without the consumer passing data around. The child component, or any of their children components, can dictate when loading is finished.

There are two limitations I've found so far that would require Svelte itself to makes changes to overcome:

  1. Wrapping Container The above <Suspense> component has to wrap its default slot in a div in order to control visibility. @brucou mentioned this as a issue in the opening post but I think it merits more focus than it's gotten lost in the subsequent discussion.

  2. Intro Animations Because we're rendering inside of hidden container, any intro animations will play out before the container is shown.

Both of these could be solved by adding a hidden attribute to <slot>, mimicking the hidden attribute in HTML. If set to true, the slot content could be rendered off screen into a DocumentFragment or equivalent and skip all transitions. When the attribute changes to false (also the default), we could attach the fragment to the appropriate spot in the DOM and kick off those transitions that have been delayed.

@MrBigBanks

This comment has been minimized.

@MrBigBanks

This comment has been minimized.

@ryansolid
Copy link

Yeah a lot has changed in understanding (and even the React implementation) since the original post was made. I think things have been better defined now and Svelte should be able to better make a decision where to go here.

Vue/Preact decided to not follow React all the way down the concurrent mode rabbit hole. In fact, what originally was considered a single thing is now arguably a couple different but related features.


  1. Suspense the placeholder. This is what those libraries implemented and more or less is what @ADantes is illustrating. In the case of a reactive library using context makes a ton of sense (as really the only way to connect the dots when we aren't doing full top down re-rendering) and one could just implement that and call it a day. This is all Vue or Preact is doing. One could implement this and get a similar experience.

  1. Transitions. This is where the conversation went off a bit. Suspense the placeholder is great for initial loads but some of those early demos showed other behaviors without really defining them. Transitions in React are where a change set is isolated and not committed until all promises that are initiated due to that change that are read under a Suspense boundary completely resolve. Ie.. if someone transitions to the next tab the actual state change of the tab change is held from the outside view until all the data is loaded for the new tab and we can update everything at the same time.

The idea here is that in order to maintain async consistency we can't let an in flight change be visible outside the scope of that change. A more concrete example might be picturing a "Like" button on a User information carousel, where you click next to load the next user. In the ideal world proposed here, one could click next starting the rendering and loading of the next user off screen, and then the enduser can click like button while this is happening and still have the current in view user record be the one that is liked as the updated user id won't have propagated yet.

Basically both rendering the next possible state (to perform data loads), while showing the current state that is completely interactive without causing any inconsistencies both in data or in visual tearing. Ie.. the page number doesn't update until everything is completed. This is the benefit of transitions.

Now React has refined this further stating that even under transitions it is only previously rendered Suspense boundaries that hold, and newly created ones instead fall to the fallback state (ie any async reads under them are not counted towards the transition). In so the enduser has the ability to create the boundaries and nested boundaries of where you want to hold or show placeholders while it frees up things like routers or design systems to just blindly implement transitions into their controls. If those transitions never trigger something async that is read under a Suspense boundary they are basically identity functions that do nothing. However if the application implements Suspense they tap into a much richer system for controlled orchestration of both placeholders and held(stale) states.

This seems complicated and it is, but the introduction of stale states save from excessive falling back to loading placeholders. You can picture with tab navigation if you are already looking at a tab and click the next one you don't need to replace the whole view with a placeholder.. a simple indicator of the stale state would be much less jarring. Browsers natively do this on page navigation. Even if a page is slow to load if it were server-rendered (ie the HTML all present when it does load) it skips ever showing the white screen. You see the current page, then some sort of loading indicator and then bam the new page. We are less accustomed to this these days because of all the client-side rendering going on and forced loading states. But the native browser handles this pretty seamlessly.


  1. Ok so clearly we can implement number 1 and call it a day like Vue or Preact. But the other discussion was interesting to see if it could be modeled in Svelte. It is probably unnecessary. But React had good reason to go this way as this same concurrent rendering in their case attached scheduling is what is going to be powering their HTML streaming and Async server rendering that is in their upcoming Server Components. Now I've talked with a few authors of other libraries and they are positive (as am I) that this is possible without concurrent rendering. Mostly that the scheduling piece can be viewed as a 3rd feature. We talked a bit about it above but that's the part that others have eluded to not really being necessary in a performant system like Svelte. If you aren't scheduling huge amounts of diffing work I mean why add this overhead, it's a solution asking for a problem.

So I know reactive frameworks like Svelte (and I've made the same wager with the reactive frameworks I am maintaining) don't need transitions to get this sort of rendering. I already have it working in both of mine without (even though I have implemented transitions in one and intend to in the other). So the 3rd feature I think is unneed here.


Analysis:

If desirable it's probably fine to go ahead with 1 and forget about it. Mind you it is a lot less interesting for a library that already has an await control flow. Like just hoist the await, pretty easy way to get the placeholder in the right spot. Like if you not concerned with trying to mimic the transition behavior of stale states, you don't need to worry about preserving anything. Vue and Preact added this Suspense component because they didn't have await.

Suspense is a bit slicker to be sure but most of that comes in actually burying the promise in a resource primitive when it comes to data loading. You can basically treat it like a typical Svelte store and not even write the clearly async await blocks in the child code. A subscription to that store under a Suspense boundary is sufficient and child components can trigger Suspense without even being the wiser they are dealing with async data. I mean it's powerful but is it worth adding a new primitive type.. a new sort of Svelte store if you may. It does have to be one since it needs to be able to persist cross component boundaries. This is a lot for only a portion of the story, a portion already mostly taken care of by Svelte.

The second feature is mind warping and really what people have been referring to when talking to this in the past. Rich in an issue a while back was talking about how to encode change sets as a way to achieve this. I've been down that road too but ultimately decided that forking the reactive primitives into transactions(while holding updating existing side effects) is probably the best way to achieve this in a reactive system. But let's face it, again this sort of quantum reactivity is a steep price for basically a navigation trick. It's a freaking cool navigation trick but you have to ensure large parts of your system are side-effect-free. This is all the grumbling you hear about the concurrent mode in React.

There are other challenges being a compiled language. It's doable with dedicated reactive primitives but for a language it is a bit trickier. You could reserve syntax for it. Like $: with await's in it could basically create the new primitives under the hood but would you really want to introduce the equivalent of the useTransition API. I mean you need it if you want this behavior to be opt in and I personally feel it needs to be opt-in as being on by default is nonsensical for end users since you'd in some cases always need to show them data in the past even after they thought they updated it. I will leave it up to the minds here to decide whether that is suitable for Svelte. But I suspect this whole thing brings with it a sort of weight that might be best left on the table. Not to say it isn't beneficial, but does it align with Svelte's goals?

@WaltzingPenguin
Copy link

I'm assuming #2 and #3 are referring to React's page on Concurrent UI Patterns? I took a stab at implementing the main features from their demo there. While it isn't production worthy, I did walk away with a very different idea of what Svelte's blocking issues are.

My biggest take away is that Svelte already has a primitive to show two different versions of a DOM tree at the same time: {#key}. While the out transition is playing, we still have a tree that is reactive, displays animations, and is acting on a copy of the stale data. That's the hardest part already done and finished.

Limitations

As noted above, that demo isn't production worthy and I'm pretty sure Svelte would need new features to make it so. Here's a few issues it has:

  • "out-in" transitions: I really want a proper way to do "out-and-then-in" transitions, so that exactly one of the out states or the intro state is displayed at a time. Something with the same functionality as Vue's "out-in" transition mode would certainly work and be useful for wide number of cases, not just this. Without that, rapidly clicking between links results in all sorts of rendering glitches in this demo.

  • Cancellable Delay: Navigating to "Fast" and "Medium" takes the same amount of time, even though "Fast" is ready much sooner. This is because I used the animation's delay to keep the page on the screen while the new data loads, but that ends up acting as a minimum time for the page transition to occur. If there was a hook to cancel the delay of a transition already in progress, we could use getContext tricks similar to the <Suspense /> demo above to call it when data loading was finished.

  • Render Off Screen: Intro animations don't play correctly. This is because, like my previous demo above, we're rendering the new page into a hidden element and the animations trigger too soon. We could render the new page in a DocumentFragment and skip out on animations until that fragment was attached to the DOM.

    I would not be sad if this was the new default behaviour for all "in" transitions with a delay. The element being added immediately to the DOM has always been something I've worked around.

  • Wrapping Div: There's still a wrapping div hanging out. For a final implementation, we'd want to find a way around that being necessary.

This entire demo does feel like an abuse of the current transition system. My point isn't that this should be a final API, merely that the underlying components needed to build a good API are mostly already present. The new parts required, while not easy, don't look particularly ground breaking and have prior art.

@WaltzingPenguin
Copy link

WaltzingPenguin commented Mar 15, 2021

A weekend of tinkering, and I have something that just more or less just straight up works:

Warts

  • Render Off Screen and Wrapping Div: Like the previous two attempts, there's wrapping <div>s we'd prefer to do without and intro/outro transitions play at the incorrect times.
  • Passing Values: <Transition bind:value let:store is gross. I had to pass down a store because <slot bind:value> isn't supported. It's made even worse that subscriptions to the store can't be made in that block, so every component has to be aware a store is being passed to it. Doubling up the declaration isn't great either.
  • Ambiguity: bind:* and on:* inside the <Transition> block are probably going to have some really weird behaviours since they'd currently be bound to both the current and loading state. I haven't tested this in any depth yet.
  • Single Value: Only one value can be bound in this implementation. It's pretty easy to extend it to cover other variables by hand, but getting Svelte components to do things based on dynamic variables names isn't an easy task.

Edit: Now that I have this working, I can't find a good use case for the Transition component. Every example I've found of React's useTransition, which is what it mimics, has effectively been just for routing purposes. We could implement a router using just Suspense and without half of the weird edge cases this Transition component creates. I'm still heavily in favor of a <Suspense> equivalent though.

@stale
Copy link

stale bot commented Jun 26, 2021

This issue 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
Copy link

stale bot commented Dec 27, 2021

This issue 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 Dec 27, 2021
@frederikhors
Copy link

Nope

@stale stale bot removed the stale-bot label Dec 27, 2021
@WaltzingPenguin
Copy link

<Suspense> and <SuspenseList> can be mostly implemented in user land. npm library here: https://www.npmjs.com/package/@svelte-drama/suspense There are some limitations that cannot be worked around, to the best of my knowledge, without core changes. Chiefly, orchestrating with intro/outro transitions and rendering during SSR.

Including something like this in Svelte core would have the advantage of making it much easier to do async SSR in general; one of the current obstacles is determining when a page is "complete" and ready to be sent, but the <Suspense> components have a known loading and complete state.

useTransition looks like it can also be implemented in userland with similar caveats. My last stab at it is here: https://svelte.dev/repl/4517e0bcf9d5488a9db74fb38f3df218?version=3.44.3 I'm not convinced useTransition is a useful tool for Svelte though. In every case I've looked at, useTransition is properly some form of routing and covered by frameworks like SvelteKit, or can be duplicated through RxJS inspired streams (see https://svelte.dev/repl/8ab0d78cc9c84582a693ef9d520f461f?version=3.44.3).

@ryansolid
Copy link

I'm not convinced useTransition is a useful tool for Svelte though. In every case I've looked at, useTransition is properly some form of routing and covered by frameworks like SvelteKit ....

I think this is fair. The navigation story is like 90% page/tab navigation. So in those scenarios there are ways for the router to render offscreen. Transitions are are sort of like the academic solution for purity. I implemented them both ways in Solid over time. Originally just using Suspense and tapping into control flow and eventually moving to forked realities and merging. Since I had that I went the full way to doing interruptible concurrent rendering. The time-slicing element is interesting but I haven't had many applications for it yet.

People will point to our performance as a reason not to bother with concurrent rendering and they aren't far off. This whole thing is about asynchronous consistency. So it is all in an effort to prevent glitches and tearing. Without framework intervention it is hard to do since scheduling will slice things the other way and not hold executing side effects. I don't think this would be easy to do with RxJS or any external driver in a generic way. It isn't about scheduling a slice of reactive updates. You really do need to basically clone things. But the problem might be too narrow to warrant a generic solution.

It also doesn't help the expense of side effects. It's funny it is always demo'd with like expensive visuals or 3d but it can't help you render faster. At some point you take the hit, and if you want to avoid tearing you take the full brunt of it. This whole mechanism is for expensive pure computations. Think $: variable in Svelte. You need to do some pretty heavy stuff for this to be your bottleneck. It turns out making 3D models is one which is why it is in 3D demos. Now you can build everything in the background without interrupting the animation, but if what you were drawing was too expensive in the first place you are out of luck.

I love how cool it demos, but I think it takes a real specific sort of problem for this because, it isn't the DOM cost which is often the heaviest thing when doing large navigation. DOM creation and attachment doesn't get interrupted. It's if the user code is doing some really expensive stuff and blocking. Is that worthy of systematic solution? I don't know.

@jdgamble555
Copy link

Did anyone from the Svelte team every comment on this issue since 2018? Would be great to get something like this implemented natively. 😀

J

@distor-sil3nt
Copy link

distor-sil3nt commented Apr 27, 2023

Is there any plans for including <Suspense> in Svelte? This is a great (though still experimental) feature in Vue which would add a lot of value for rendering with async child components.

@intrnl
Copy link

intrnl commented Nov 15, 2023

Now that we're in the land of signals alongside Solid.js and all, any thoughts on Suspense from the Svelte team?

Specifically with regards to the ability of being able to conditionally pause effects/renders/updates from a component subtree, this is where I've found Suspense to be the most beneficial with.

Take for example, mobile app navigations, they are stacked and ideally you don't want to rerender any views that aren't currently on screen, but you can't unmount them or else you lose all the states within them, not unless they're stored within a global state (which creates additional complexity). This example is very much real, this is how Suspense is being used within React Native apps, and could end up benefitting Svelte Native too.

Other sorts of navigations like tabs, they all benefit from being able to keep offscreen components mounted, yet frozen such that UI updates don't affect them until they are no longer frozen.

Personally I think Suspense is a very beneficial thing to have, just not in a manner that React thinks it's beneficial for.

@njacob1001
Copy link

This would be a nice performance enhancement! other frameworks like Angular are introducing new approaches to get this feature on its primary tools

@stefa168
Copy link

stefa168 commented Sep 5, 2024

It would be amazing to see the Suspense mechanisms come to Svelte 5! +1 for this :)

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