solid-start icon indicating copy to clipboard operation
solid-start copied to clipboard

SSR with `<Suspense/>` doesn't work anymore as it used to

Open BierDav opened this issue 3 months ago • 4 comments

Duplicates

  • [X] I have searched the existing issues

Latest version

  • [X] I have tested the latest version

Current behavior 😯

The current behavior when using Suspense in ssr in 0.7.6 is that it renders the content in a <template/>

Let's look at this example I provided: https://codesandbox.io/p/github/BierDav/solid-start-ssr-bug/master

When we look at the working example at the network tab and preview the initial html provided we see everything looks fine: image The list is rendered and the style is applied

But on the not-working page, which uses a createResource, everything in the suspense is seamingly missing: image

If we look closely than we see that the content of the Suspense can be found in the initial html, but it is contained in a template which makes it invisible in the preview and also in the initial view the user gets: image

This is a different behaviour than in 0.3.10 from which I recently upgraded. Because in this version the result of Suspense is directly rendered in the body and this is what I expect because doing it the new way has two major disadvatages:

  1. The user again has to wait till the javascript kicks in, which kills all the advantage of ssr
  2. There are issues with methods like useAssets which makes libraries that heavily rely on this as for example suid unusable

Note: I can be that the issue with useAssets in a <For/> is unrelated, but I was not sure and didn't want to make a second example. As you can see in the example, all the coffee entries are full width, because the css applied using useAssets which should make them 200px wide doesn't work in a <For/>

Expected behavior 🤔

I want that ssr works again as it already was, that it renders directly into the body and that useAssets does work even when used in a <For/>

Steps to reproduce 🕹

No response

Context 🔦

I want to point out that everything worked fine in 0.3.10, but they broke in the recent versions. I would really love a quick fix for that, because I have already ploughed up our whole codebase.

Your environment 🌎

No response

BierDav avatar Mar 20 '24 08:03 BierDav

It works as intended: the resource is called, triggering a Suspense boundary, where the resolving UI is streamed after the initial page load.

The only reason useAssets didn't work is because it's a server-only API and is never meant to be used isomorphically. And if we consider the SSR setting, the useAssets is called only after the fact that the initial html has been sent.

However, a probable solution here is to add deferStream: true to createResource, essentially turning it into a blocking resource rather than a streaming resource.

lxsmnsyc avatar Mar 20 '24 09:03 lxsmnsyc

Thanks for your answer, I understand that useAssets is only server-only, but that's quite inconvenient when useAssets doesn't work for ssr that is streamed. Is there a solution to this? Btw. what is actually streamed, the hydratable html or only the data?

And why doesn't useAssets work on the working page of my example, which should be completely rendered on the server without suspending:

<For each={data()}>
          {(coffee) => {
            useAssets(() => (
              <style>
                {`
                        #coffee-${coffee.id} {
                       width: 200px
                        }
                        `}
              </style>
            ));
            return <CoffeeItem coffee={coffee} />;
          }}
        </For>

But when it is a streaming resource why is the result directly in the initial response?

BierDav avatar Mar 20 '24 10:03 BierDav

Thanks for your answer, I understand that useAssets is only server-only, but that's quite inconvenient when useAssets doesn't work for ssr that is streamed. Is there a solution to this?

Like I mentioned, you add deferStream: true to the resource that you want to "await" before sending the initial HTML.

const [data] = createResource(fetchData, {
  deferStream: true,
});

return (
  <Suspense>
    <h1>{data()}</h1>
  </Suspense>
);

Btw. what is actually streamed, the hydratable html or only the data?

SSR sends first the initial HTML. If any Suspense boundary is triggered and the resource that triggered it is a streaming resource, the initial HTML only includes the fallback UI. When the resource resolves, the data is streamed to the client, alongside the "success" UI of the Suspense (this is the template element you are seeing).

And why doesn't useAssets work on the working page of my example, which should be completely rendered on the server without suspending:

If you're referring to the snippet combined with createResource, it still won't work, as it behaves in a similar way as your original repro. Point is once the resource resolves and Suspense tries to render the success UI, the initial HTML has already been sent, so there's no way to insert the new styles.

lxsmnsyc avatar Mar 20 '24 11:03 lxsmnsyc

When the resource resolves, the data is streamed to the client, alongside the "success" UI of the Suspense (this is the template element you are seeing).

Uhh ok that makes sense. But the point I wanted to make is that when the "success" UI is rendered on the server how should I be able to add styles inside the "success" UI, when I can't use useAssets, because the initial html is already sent and on the other hand the "success" UI is rendered on the server so I don't have access to the client.

Additionally what I don't understand is why not already inline the template when the "success" page is already available? I mean I can clearly see that in the template that is contained in the initial html the data is already filled in. I don't know if that was excatly the behaviour, but I would love to see a hybrid solution. So that all data that is responed before the initial html is sent is instantly inlined so that the user never sees the loading fallback and if the data fetching takes longer than ssr than we dynamically switch to streaming.

I cannot imagine that this is so difficult, because as pointed out the template where everything is there is included in the initial html. So clearly there must be some logic that optimizes that and adds the template immeditely without requireing to stream it. It just doesn't inline it.

Or is there a catch i didn't think of yet?

If you're referring to the snippet combined with createResource, it still won't work,

No I actually was referring to the snippet without createResource, which should in theory work, but it unfortunately doesn't

BierDav avatar Mar 20 '24 12:03 BierDav

Additionally what I don't understand is why not already inline the template when the "success" page is already available?

The thing is, it isn't. The same concept applies to how this doesn't work:

let example = 'Loading';

Promise.resolve().then(() => {
  example = 'Success';
});

console.log(example); // Loading

I mean I can clearly see that in the template that is contained in the initial html the data is already filled in. I don't know if that was excatly the behaviour, but I would love to see a hybrid solution

Browsers can batch chunks but the server doesn't know this and cannot make this kind of assumption.

I don't know if that was excatly the behaviour, but I would love to see a hybrid solution. So that all data that is responed before the initial html is sent is instantly inlined so that the user never sees the loading fallback and if the data fetching takes longer than ssr than we dynamically switch to streaming.

Which is what I recommended: deferStream: true

lxsmnsyc avatar Mar 20 '24 15:03 lxsmnsyc

Ok so tell me if I understood that correctly. Streaming means that the solid server renders everything until it hits a suspend boundary and immediately sends this to the browser, but it doesn't close the response stream. Instead it waits for all those resources and each time one finishes it renders it and emits a template in the response stream.

But why does the browser even render the html page before it even has the whole picture, elements on the end of the document could change the whole page?

Nevertheless, I know now what I want. I want a threshold that I can specify. Per default all resources should be evaluated deferred unless they take longer than the specified threshold (lets call it 300ms), after that period of time the server finishes rendering the rest and streaming the remaining resources when they have finished. This combines both worlds and optimizes layout shift and time to first byte.

Last but not least, I still need an alternative to useAssets so that we can push styles when streaming, this is a must have if we want to support streaming, because some conditional styles cannot be known ahead of time. Do you have any idea how I could solve that. With that knowledge I would make a pr to suid so that it also supports server side streaming or how this is called.

BierDav avatar Mar 20 '24 15:03 BierDav

But why does the browser even render the html page before it even has the whole picture, elements on the end of the document could change the whole page?

The whole concept of streaming is so that the browser can immediately show the page as early as possible. Streaming utilizes that fact and then sends the lazier chunks after it. Solid's async SSR is the complete opposite of this: it waits for everything before sending the entire HTML.

lxsmnsyc avatar Mar 20 '24 16:03 lxsmnsyc

Thanks for your detailed explanation. But what about useAssets in streaming mode?

BierDav avatar Mar 20 '24 17:03 BierDav

If you want the 0.3.10 async default you can just pass it in to createHandler. We default to streaming now but you can have the previous behavior.

createHandler(() => <StartServer />, { mode: "async" })

Streaming we've been using SolidMeta to do the split between assets that come from useAssets and adding them on the page in the client. We are looking at isomorphic handlers for assets that work seamlessly with streaming in Solid core. @lxsmnsyc has opened some PRs that I'm yet to review.

ryansolid avatar Mar 20 '24 17:03 ryansolid

Ahh thanks for your info, the thing with the async mode I have also already found out, but thanks for making it clear.

I will close the issue now, because everything is clear now and this has been more a discussion than an issue.

Have a nice day, @lxsmnsyc and @ryansolid

BierDav avatar Mar 20 '24 18:03 BierDav