solid-start
solid-start copied to clipboard
SSR with `<Suspense/>` doesn't work anymore as it used to
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:
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:
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:
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:
- The user again has to wait till the javascript kicks in, which kills all the advantage of ssr
- There are issues with methods like
useAssets
which makes libraries that heavily rely on this as for examplesuid
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
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.
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?
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.
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
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
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.
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.
Thanks for your detailed explanation. But what about useAssets
in streaming mode?
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.
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