Replies: 5 comments 2 replies
|
This is a really interesting idea for the framework! Dynamic rendering like this is a solid way to balance performance and SEO, especially since it avoids that 'double data' overhead you mentioned. A few technical thoughts from someone who has implemented similar patterns:
|
|
I realised that point 3. (faster data than regular SPA by streaming it in) and 4. (no double data problem) needs a combined elaboration. Both of these cases involve kicking off the data requests on the initial request on the server (instead of on first render on client). I see two ways of achieving both of these in unison: Blocking or Streaming (deferred loaders). Blocking:On first request, server waits for the loader, before returning SPA shell + HTML embedded JSON payload together in the first HTML response. You could have a loader that synchronously (blockingly) runs on the server on the initial request, to just return a little bit of DB data, so the response to the user remains fast. The client gets the SPA shell + that little data, renders it, the user starts consuming it (e.g. a post), and then the SPA could use Suspense to lazy-load the rest of the data (e.g. comments to that post) in the meanwhile. In the SSR for bots case the lazy-loading is eagerly/blockingly executed, so the bot gets the full HTML on first request. Details (click to expand, perplexity.ai generated code)Unified Page Component (
|
|
Yea I looked at something this recently. We do have a few things you can do today I think - One is you can render layouts as ssg or ssr only, but not the inner page, that's a nice balance. Another is you can have an SSR route but just bail early depending on the request. Just have the loader return something from the request you can then use to decide if you want to render the , add a supress hydration warning. If it's a bot you return if not you return full page. Only thing I'm not sure about there is caching, we may want to have some ability for loaders to set headers, I guess you can call our set headers helper but not sure if our docs are clear there and how well tested that it. |
|
One operational concern worth making explicit is cache correctness. If bot-rendered HTML is cached separately, the cache key needs to vary by the crawler decision and the rendered URL, while the rendered content, canonical, and status code should still match what a user can reach. This is a reasonable fit when a team needs to keep an existing SPA rather than migrate the app. I work on PageFlash, which provides crawler rendering and cache operations for registered public SPA routes: https://pageflash.io. It is not a substitute for SSR/SSG when those are feasible, but it can reduce the migration pressure in this specific setup. |
|
Building upon the idea, this is how core Web Vitals could be optimized with this architecture. The comparison uses React Relay, but it could also be done with an equivalent non-GraphQL approach as long as it also basically injects a data store JSON payload into the HTML shell, which the client should use for the first hydration-less render. Gemini AI response to me massaging this architecture a bit: This is precisely how modern high-performance streaming architectures operate. By leveraging Chunked Transfer Encoding, you decouple the initial document head from the data payload. This creates an optimal execution pipeline: The Streaming Shell + Pre-Primed Cache Pipeline
Why This is Architecturally Brilliant
NB: This is of course not a real benchmark, just Gemini's assessment of how it would likely play out, based on the tradeoffs. Head-to-Head Performance Comparison (Streaming Architecture + Edge CDN Assets)
vs. SSR:Unlike classic Server-Side Rendering—where the server burns heavy CPU cycles stringifying massive component trees for every visitor and the browser pays a steep main-thread tax to hydrate them—this architecture dramatically slashes infrastructure costs and completely eliminates hydration bottlenecks by only deploying full SSR for search engine crawlers via User-Agent detection, while serving human users a lightweight, unrendered shell paired with a pre-primed data payload. By shifting your backend from an expensive rendering engine into a lean data proxy that streams raw data alongside an unrendered shell, the client performs a clean, single-pass initial mount from an in-memory cache, saving substantial server compute resources while wiping out hydration mismatch bugs, bypassing the main-thread-blocking hydration tax, and delivering a lightning-fast, buttery-smooth runtime experience to humans alongside flawless SEO indexing for bots. vs. SPA:Unlike a classic Single Page Application (SPA)—where the browser must download the JavaScript bundle, boot up, and then make a separate client-side network request just to fetch its initial data before displaying anything—the pre-primed streaming shell approach eliminates this data-fetching waterfall entirely. By having the server handle the initial data fetch concurrently and injecting that payload directly into the streaming HTML document, the client receives the UI code and the data simultaneously, allowing React to mount the fully populated application instantly on the very first render without any loading spinners or delayed API round-trips. Summary of Pre-Primed Architecture Optimizations
Implementing This Without React RelayThe core pattern—streaming a shell alongside a pre-loaded state payload—is entirely agnostic to Relay and can be achieved with any state management tool or standard React setup:
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
A unique selling point for One could be that it makes it easy/streamlined to have SSR for SEO, but not incurring the rendering cost/time of SSR on the server for human users (they would get the SPA instead). SPA also aligns with the mental model for both PWA and React Native apps.
Cheaper server cost, due to less server processing time needed due to not rendering the HTML on the server for users. Offload the HTML rendering to the user's device.
No hydration needed, as opposed to SSR: Yes, doing this may mean that the first render of the HTML containing all the data is slightly slower for users (but the HTML shell arrives faster), since all the data in the HTML does not come to the client at first load. But that means that the client doesn't need to hydrate the HTML either, so it removes the annoying "visual but unclickable" state of pages that appear during hydration. If the SPA shell is served from the Edge (Cloudflare function or static CDN), then the user may even see the HTML shell faster than if waiting for SSR. (Besides, you could extract some data from the URL and show that in the HTML shell, like the post title, without needing to hit the DB.)
Faster data than regular SPA, by streaming it in: If, on the initial server request, the DB requests are kicked off immediately, and then the data is subsequently streamed in to the client, then you also avoid the latency that SPA's normally incur when kicking off the data requests on the client. (Requires a new feature: deferred loaders.)
No double data problem (costs time and bandwidth): You avoid the "double data problem" that NextJS has, which occurs when the same data is fetched and sent to the client twice: once embedded in the server-rendered HTML, and again in the JSON payload (NEXT_DATA) needed for client-side hydration with React. Downloading the data twice increases is a cost to the client in terms of download time (which increases TTI) and network usage (mobile $$$; could be important for low income countries), and time client side parsing overhead. Basically, One would only send the data once as a JSON payload (via a global script tag in the HTML head or body), for React to render (on the client), not also pre-rendered (on the server) inside the App structure in the HTML.
How could this could look without needing to have duplicate
page+ssr.tsxandpage+spa.tsxpages for every page:perplexity.ai generated response:
Dynamic SSR in Middleware (Single
page.tsx)Use one
page.tsx(due to global SPA/default mode) + middleware that fully renders DB content on-the-fly for bots:Why This Works
page.tsx(handles both client SPA + server props).Make
page.tsxacceptdataprop (server-fetched for bots, client-fetched for users). This is the standard "dynamic rendering" pattern—cleanest DRY solution without framework hacks. [reddit](https://www.reddit.com/r/TechSEO/comments/suqnqc/main_differences_between_prerendering_and_dynamic/)All reactions