overlookmotel/react-async-ssr

Streaming rendering

Opened this issue · 10 comments

Implement streaming rendering.

This would be tricky to implement. At present a tree of Suspense + Lazy components is built during rendering, and then the tree is converted to HTML output at the end. Streaming rendering would require collapsing the tree as lazy nodes resolve and outputting HTML when Suspense boundaries are fulfilled.

The tricky part is dealing with edge cases around NO_SSR promises inside other lazy nodes which may trigger a Suspense fallback at arbitrary times.

As an optimization, it could be useful to allow the user to indicate that a Suspense boundary will not have its fallback triggered with a property on the <Suspense> element. Then the contents of that boundary can be streamed out as they become available, rather than waiting for the whole contents of that Suspense to resolve.

e.g.:

<Suspense allSsr fallback={<div>Loading...</div>}>
  <Lazy1/>
  <Lazy2/>
</Suspense>

If a NO_SSR promise is thrown within a Suspense boundary marked as allSsr, the stream would emit an error.

Was just thinking about this after watching the suspense presentations and thought how it could be implemented.

TREE -> render -> output html tree with fallbacks rendered & emit -> start resolving boundaries -> emit html for every fulfilled boundary

As to the edge case, NO_SSR promises could fulfil with a flag that will let the client (React internals) know it needs to handle the boundary on the client. Not even sure it needs to be React internal, might be able to do it in user land, just wrap the Suspense in a 3rd party wrapper to hold flags

On Server: Boundary[no_ssr] -> resolve fallback BUT! add internal flag to boundary to try again on client

@oviava If I understand you right, what you're suggesting is that streaming rendering should not wait for suspended elements to unsuspend, just render fallbacks on the server, and leave rendering these elements to the client.

This would be quite easy to implement, but I actually think it'd be better if suspended elements are awaited and rendered on server side. i.e. the HTML sent to client when streaming is identical to what you get from .renderToStringAsync(). The only difference would be that response is streamed to the client as fast as possible, rather than waiting for the entire server-side render to complete and then sending the response to client.

Without this, we lose a lot of the advantages of SSR. e.g. the pages search engines index would lack a lot of the page content.

But unfortunately this is quite complicated to implement!

By the way, on this:

On Server: Boundary[no_ssr] -> resolve fallback BUT! add internal flag to boundary to try again on client

That is precisely what .renderToStringAsync() does at present.

I had a small chat with Dan Abramov via twitter, they're planning to do something like that with Suspense on SSR - my first suggestion. He didn't go into details as they haven't fully decided on the API but the base idea is like this

TREE -> render -> output html tree with fallbacks rendered & emit -> start resolving boundaries -> emit html for every fulfilled boundary

@oviava Interesting. Could you possibly post a link to the twitter thread?

It may be that React can do things that this package can't because obviously they have control over the entire codebase (both server-side and client-side).

The purpose of this package is just to be a stopgap until this is implemented in React. But judging by the speed of progress so far, I'm guessing this won't be until JSConf this time next year. I didn't want to wait that long!

I've designed this package so it only requires extra code on server-side, but produces HTML output which can hydrate correctly only using standard React + ReactDOM on the client. I didn't want to produce a client-side package too.

It was a private convo but I think it should be ok, no personal info and nothing out of the ordinary IMGUR Screenshots

Thanks for sharing that. That's really interesting.

To be honest, though, I see some problems with it.

I mean I see that it's a very smart solution for when a user is loading the page in the browser. But what about web crawlers e.g. Google?

If the suspended chunks follow the unsuspended page content, then a web crawler would get the page content in the wrong order. That could (a) mangle the page or (b) affect SEO as perhaps what's meant to be the most salient content would appear to the web crawler as being at the bottom of the page - which may cause the search engine to give less weight to it.

For me personally, the primary attraction of SSR is that it allows your pages to be read by (and so included in) search engines, so this would be a bit of a deal-breaker.

Or am I missing something?

That's your choice I guess, I imagine you could have the choice NOT to do that, just the possibility.

My biggest hope for this is we might finally be able to individually cache component renders and stream them to client - something that would be amazing. In SSR, different 'parts' of the page can have different caching strategies, eg: for an online retailer being able to cache product pages, or product components, or headers or stuff that has relatively long life times would be very very useful and at the same time mix them in page with highly dynamic components (basket, financial info etc.). All under the same tree, there are strategies to do this even now, but it'd be much easier doing it all under React's API

Also, as I understand google's crawler has become VERY prolific at handling SPA's.