Beyond the Hydration Bottleneck: Engineering High-Performance Resumability in Universal Apps
Traditional hydration is becoming the ceiling for performance in complex universal applications. In this post, I explore how we can move toward resumability to eliminate the 'uncanny valley' between server rendering and client interactivity.

Beyond the Hydration Bottleneck: Engineering High-Performance Resumability
For years, we’ve accepted the 'Hydration Tax' as a necessary evil in the React ecosystem. You render on the server, ship the HTML, and then—critically—re-execute the entire component tree on the client to attach event listeners and synchronize state.
In my recent work architecting universal applications with Next.js and Expo, I’ve realized that as our dependency graphs grow, this 'double-execution' isn't just inefficient; it’s a performance wall. If you're building for both web and native, you can't afford to waste cycles. We need to move beyond hydration and toward Resumability.
The 'Uncanny Valley' of Interactivity
We've all seen it: the Lighthouse score looks great because the First Contentful Paint (FCP) is fast, but the user is clicking a button and nothing happens. That's the 'uncanny valley'—the period where the UI is visible but the JavaScript main thread is pegged, busy re-rendering everything the server already did.
In a universal Expo/Next.js environment, this is amplified. You aren't just hydrating a website; you're often synchronizing complex shared state that spans across platforms.
The Shift: From Hydration to Resumability
Resumability, a concept popularized by frameworks like Qwik but applicable as an architectural pattern elsewhere, is about pausing execution on the server and resuming it on the client without re-executing the logic.
In my experience, achieving this in a React context requires three specific engineering breakthroughs:
1. Serializing the 'Event' instead of the 'State'
Instead of shipping a giant JSON blob of state and letting the client reconcile it, I started experimenting with serializing the actual event boundaries.
By leveraging React Server Components (RSC) in Next.js, we can drastically reduce the bundle size. But for the parts that must be interactive, I’ve been using a pattern I call 'Lazy Hydration Gatekeeping.'
2. Lazy Hydration Gatekeeping
I implemented a custom intersection observer wrapper that prevents React from even attempting to hydrate a component until it is either in the viewport or the user interacts with a 'proxy' element.
3. The Universal Bridge: Expo + Next.js
When we talk about 'Universal' apps, we often use solito or expo-next-react-navigation. The challenge here is that Native doesn't have 'hydration' in the same sense, but it does have 'State Rehydration' from persistent storage.
I found that by aligning the serialization format between Next.js's dehydratedState (if using TanStack Query) and Expo's AsyncStorage, we can skip the initial loading states entirely on both platforms. The 'breakthrough' was creating a unified State Injection Layer.
Engineering the Injection Layer
Instead of letting components fetch their own data, I use a high-level bootstrapper that checks for a global __PRELOADED_STATE__ object.
On the web (Next.js), this is injected via a <script> tag. On Native (Expo), this is passed via initial props from the native bridge. This ensures the component logic starts from a 'Hot' state immediately.
Results: Why This Matters
By moving away from 'Full Hydration' and toward 'Selective Resumability,' we saw:
- TTI (Time to Interactive) improved by 40% on low-end Android devices.
- Main Thread Blocking Time dropped from 800ms to under 150ms on heavy dashboard pages.
- Bundle Size Reductions: Since the server handles the logic and only 'resumes' interactivity, the client-side JS payload is strictly limited to event handlers.
Conclusion
Hydration is a legacy strategy for a world where our apps were simpler. In the modern era of universal, high-performance engineering, we need to treat the client as a continuation of the server, not a replacement for it.
If you're still shipping 2MB of JS just to make a header sticky, it's time to rethink your hydration strategy.