There is some great discourse going on about how <Suspense>
timing should work in react@19
.
In this post I explore what a <Suspense>
algorithm would look like if all pending promises in a <Suspense>
boundary were resolved before trying to re-render.
This started as a scratch pad, then a proposal, then I saw some problems with the approach... but I thought I would record and share it all the same. Maybe it might lead to some other better ideas!
TLDR
'Wait for pending' is an interesting variation of the react@18
and react@19
(alpha) <Suspense>
algorithms. For flat async trees, 'wait for pending' allows for fast calling of render functions, and minimal re-renders. For trees with nested async components, child async components will have their initial render called slower than the react@18
algorithm.
Background
<Suspense>
in react@18
- render all possible components inside a
<Suspense>
boundary. - Re-render when any promise resolves
- Continue until no more components throw a promise
Results in lots of rerenders, but allows parallelization of 'fetch in render' calls, and all components will have their render called as quickly as possible.
<Suspense>
in react@19
(alpha)
- stop rendering tree when a component throws a promise
- wait for promise to resolve
- re-render tree
- Continue until no more components throw a promise
Results in minimal rerenders - but causes 'fetch in render' calls to be sequential (waterfall)
'Wait for pending' algorithm
Here is an idea for <Suspense>
timing ('wait for pending'):
- Always render siblings, even when they throw (like
Suspense
inreact@18
) - Don't re-render children until all currently thrown promises are resolved.
Let's see how it goes!
'Wait for pending' is similar to the current react@18
<Suspense>
algorithm, except that rather than rendering all children when any thrown promise resolves, only render when all currently thrown promises resolve.
- Allows for 'fetch in render' in siblings to trigger parallel fetches
- Still has a great story for pre-fetching
- Reduces the waste caused by re-rendering possibly expensive components
- Expensive component renders along side siblings that
throw
will still be redundant. But, at least these redundant renders are reduced - 👎 Can slow down nested 'fetch in render' calls (see below)
Rough algorithm
- render children
- if no thrown promises, done - otherwise go to step 3
- wait for all thrown promises to resolve
- go to step 1.
Example 1: Only siblings
<Suspense fallback={'loading'}>
<A />
<B />
<Suspense>
In this example, both A
and B
have a fetch
for data in their render
react@18
Render 1
-
A
renders, but throws a promise -
B
renders, but throws a promise
Render 2
- promise from
A
resolves -
A
renders -
B
renders, but throws a promise
Render 3
- promise from
B
resolves -
A
renders -
B
renders
react@19
alpha timing
Render 1
-
A
renders, but throws a promise
Render 2
- promise from
A
resolves - render
A
- render
B
, butB
throws a promise
Render 3
- promise from
B
resolves - render
A
- render
B
😢 causes waterfalls if you fetch (throw) in renders
😊 avoids excessive re-rendering potentially expensive components
Wait for pending
Render 1
-
A
renders, but throws a promise -
B
renders, but throws a promise - wait for
A
andB
to resolve
Render 2
-
A
renders -
B
renders
✅ In this case, the proposed algorithm yields great results!
Example 2: With children
Here is where 'wait for pending' strains.
Now A
renders children ChildX
and ChildY
which also do a 'fetch in render'
<Suspense fallback={'loading'}>
<A>
<ChildX />
<ChildY />
</A>
<B />
<Suspense>
react@18
algorithm
Render 1
-
A
renders, but throws a promise -
B
renders, but throws a promise
Render 2
- promise thrown by
A
resolves -
A
renders -
ChildX
renders, but throws a promise -
ChildY
renders, but throws a promise -
B
renders, but throws a promise
Render 3
- promise thrown by
ChildX
resolves -
A
renders -
ChildX
renders -
ChildY
renders, but throws a promise -
B
renders, but throws a promise
Render 4
- promise thrown from
ChildY
resolves -
A
renders -
ChildX
renders -
ChildY
renders -
B
renders, but throws a promise
Render 5
- promise thrown from
B
resolves -
A
renders -
ChildX
renders -
ChildY
renders -
B
renders
✅ ChildX
and ChildY
get rendered as early as possible
😢 Lots of redundant re-rendering
react@19
alpha algorithm
Render 1
-
A
renders, but throws a promise
Render 2
- promise from
A
resolves - render
A
- render
ChildX
, butB
throws a promise
Render 3
- promise from
ChildX
resolves - render
A
- render
ChildX
- render
ChildY
, butChildY
throws a promise
Render 4
- promise from
ChildY
resolves - render
A
- render
ChildX
- render
ChildY
- render
B
, butB
throws a promise
Render 5
- promise from
B
resolves - render
A
- render
ChildX
- render
ChildY
- render
B
Proposed algorithm
Render 1
-
A
renders, but throws a promise -
B
renders, but throws a promise - wait for
A
andB
to resolve
Render 2
- promise thrown by
A
andB
resolve -
A
renders -
ChildX
renders, but throws a promise -
ChildY
renders, but throws a promise -
B
renders - wait for
ChildX
andChildY
to resolve
Render 3
- promise thrown by
ChildX
andChildY
resolve -
A
renders -
ChildX
renders -
ChildY
renders -
B
renders
✅ A lot less redundant rendering than the react@18
algorithm
👎 ChildX
and ChildY
need to wait for B
to resolve before kicking off their 'fetch in render' calls. They had to wait for the slowest sibling of their parent to resolve before they could kick off their promises.
🤔 More parellisation than the react@19
algorithm, but slower to kick off initial renders for all components than react@18
.
Closing thoughts
'Wait for pending' is an interesting approach.
For flat async trees, 'wait for pending' allows for fast calling of render functions, and minimal re-renders. However, when there trees with nested async components, async child components have to wait for their parents siblings to finish rendering before their initial render function is called. If the nested component was doing an expensive operation (such as a network call), then triggering the initial renders as quickly as possible is ideal (the react@18
algorithm). The 'wait for pending' is similar to the react@19
approach - except that each level can be parellised.
It was interesting to think about what a different <Suspense>
algorithm could look like! Thanks for making it this far 😅.
Cheers