Async hydration in Preact
Written on July 13, 2026
Async hydration in Preact
I've written before about hydration in a previous post,<br>but I feel like we are overdue to revisit the topic. It might be worth catching up on<br>that post, but the TL;DR is that hydration is one synchronous process where Preact<br>compares the HTML it received from the server and makes it interactive through the<br>executed JavaScript representation of the Virtual DOM. Each DOM node is visited and<br>we apply the event-handlers, refs, and other details described by the Virtual DOM. When<br>we see a discrepancy, we abort hydration and treat it as a mismatch.
Why does the synchronous part matter? Because it's all-or-nothing: nothing on the page is<br>interactive until every piece of JavaScript and data the tree needs has arrived and<br>executed, and on a large tree the hydration pass itself can occupy the main thread long<br>enough for a click to go unanswered. The bigger your app, the longer your users stare at<br>a page that looks ready but isn't.
This synchronous behavior is often treated as an inherent limitation of Preact. Other<br>technologies like React have selective hydration, and people discount Preact for not<br>having it because… well, we aren't great at talking about these topics. If at this point<br>you are blaming me for not mentioning it before, then you have all the reason to.
The point of this post is that Preact isn't limited to this all-or-nothing model. Resumed<br>hydration already lets Suspense boundaries hydrate independently. Hydration 2.0 in<br>Preact 11 changes how we identify the DOM owned by those boundaries, while streaming<br>changes when that DOM reaches the browser. These are related steps, but they aren't the<br>same feature.
Enter resumed hydration
I never really settled on naming the practice we have in Preact, but I often refer to it<br>as resumed hydration. Its async nature is achieved through Suspense. In Preact, we<br>support that through the compat package, the preact-suspense package, and the<br>preact-iso router. All of those can opt in to resumed hydration.
There are two topics to consider here: the server and the client. On the server we<br>historically used renderToString to render our Virtual DOM to an HTML string that the<br>client can visualize for the user. When we enter this resumed hydration world, we<br>leverage renderToStringAsync instead. It isn't that different, apart from the fact that<br>it will await any suspending Virtual DOM node and continue creating the HTML string<br>from each resolved piece.
On the client we start hydrating the HTML string we received. When a child suspends,<br>Preact pauses hydration for that subtree, remembers where it stopped in the<br>server-rendered DOM, and continues hydrating the rest of the application. Once the<br>missing data or JavaScript resolves, Preact returns to that subtree and makes it<br>interactive.
This pause-and-continue behavior is resumed hydration, and it already exists today.<br>Hydration 2.0 doesn't introduce it; it changes how Preact records the DOM belonging to<br>each paused subtree. While a subtree is paused, its server-rendered view remains visible<br>and we don't replace it with the Suspense fallback.
There is also less work blocking the main thread at once. While a boundary is waiting for<br>code or data, the browser can render, respond to input, and run other work instead of<br>spending that time hydrating a subtree that isn't ready. Hydration still uses the main<br>thread when each boundary resumes, but it happens in smaller pieces rather than one long<br>all-or-nothing pass. Meanwhile, the parts that have already hydrated are interactive.
Resumed hydration flowServer rendering awaits async boundaries before sending HTML. The client hydrates the shell, pauses boundaries that aren't ready, and resumes them as their data or JavaScript arrives.One HTML tree, several resumable boundariesserverrender shellrenderToStringAsyncawait dataSuspense Aawait codeSuspense Bfinished HTML is already visibleclienthydrate shellattach handlerspause boundarywait for Aresume boundarycontinue Bfallback is skipped during hydration because the real DOM is already there<br>Resumed hydration keeps the server HTML visible, then fills in interactivity as each Suspense boundary can continue.<br>So how does this stack up against React's selective hydration? In practice it covers most<br>of what people mean when they bring it up: boundaries hydrate independently, and the<br>server-rendered content stays visible while they do. The difference is<br>prioritization: React uses interaction as a signal and will hydrate the boundary you<br>just clicked first, while Preact currently resumes boundaries in the order their code or<br>data resolves.
Hydration 2.0
This practice brings footguns with it, as all of them do… The original resumed-hydration<br>algorithm remembered 1 and only 1 DOM node at which to continue. That effectively<br>assumed that a suspending VNode would eventually produce exactly one DOM node.
That assumption breaks when the VNode produces no DOM, such...