Solid: SSR option?

Created on 3 Sep 2019  路  7Comments  路  Source: ryansolid/solid

enhancement

Most helpful comment

I made a small example of how the client-side works to rehydrate. https://codesandbox.io/s/solid-hydration-ri3jn. I think most of the more interesting parts are going to be around SSR patterns/best practices for making apps. I'm not sure where that sits, especially as I have no experience in this area and it seems extensive. As opportunities arise I will try to take this further, but now that the mechanism is there I'm going to shift my focus elsewhere for the time being.

All 7 comments

I guess nice to get it on paper here. I've been looking at this here and there the last couple months. I wanted to finish up my work on Suspense first.

Rendering on the server should not be terribly difficult using things like JSDOM. I haven't come up with an official solution as of yet, because the more challenging problem is re-hydration. Even with a solution to serialize the data state from the server, preventing complete re-render in the client is interesting challenge for a system that is not based on patching. That being said having Babel transcode to a slightly different version seems completely doable (it is how Svelte handles this).

Unfortunately Svelte is the closest library to have implemented it and it isn't even that close. No libraries that I'm aware of that use these techniques have SSR currently. I have an idea of how I'd approach this but I haven't implemented it yet. I'm going to write a few notes down here for future consideration.


All templates are a result of cloning and then applying reactive graph overtop. So as long as we get the right element reference we should be able to apply the bindings on it naturally. So if we track the current element as we move through inserts on the initial pass we should be able to replace:

const $el = $tmpl.firstChild.cloneNode(true);

with:

const $el = rehydrate ? __getNextElement() : $tmpl.firstChild.cloneNode(true);

Challenge is knowing if the next element is firstChild or nextSibling of the previous invocation. This approach would be difficult to error check. If things were not exactly where expected it would be hard to predict the results. Ensuring the right html element might be doable.. maybe passing the template in. Something like:

const $el = (rehydrate && __getNextElement('nextSibling', $tmpl.firstChild)) || $tmpl.firstChild.cloneNode(true);

Beyond that all we have to do is make sure under rehydration insert uses replaceChild instead of appendChild or insertBefore. There are probably some edge cases here. I expect undoing array normalization might prove a difficult issue in itself.

niceeee!!! I will look forward to this 馃帀 馃帀 馃帀

Mostly note for myself. For the most part should be simpler than I was thinking. Don't to worry about insertion as diffing already in place. Also generated code already tracks insertion points and parentNode so tracking fairly straightforward. The challenge is this scenario:

<>
  <ComponentA />
  <ComponentB />
</>

Looking at the rendered DOM where ComponentB start and ComponentA end? I use previous state to figure it out but on hydrate there is none. Right now all I can think of is adding a 3rd mode, 'server', which renders markers into the template ala lit-html around each insertion point. This output can be parsed by hydrate. The code output of compiler would be similar for both 'hydrate' and 'server' mode.

Ok, I have a plan to make this work. Modifying the server-rendered part of the code was trivial. It requires wrapping both sides of all insertion points with comment nodes. It's a minor cost for the benefit. The client hydration part can be inserted in the generated code in the form of getNextElement and getNextMarker. This is completely tree-shakeable so it can sit in the runtime without incurring any cost unless the compiler is in hydrate mode.

From here this should all work pretty well. Initial code will parse out markers and existing content nodes to set up the basis for future comparison. I will need some help understanding the expected behaviour of more complex scenarios though. I haven't used enough SSR to understand how Async data would work. If it was server rendered but then it took time for the client to get then the client would wipe out the server-rendered content on diff since it wouldn't be there at client hydration time. Obvious Async data is one thing and it could be rendered into the page and loaded into stores etc.. But what about async flows like dynamic imports ie.. Lazy component loading and Suspense.

I will likely look into this at some point but if anyone has any idea what the desired behaviour. I have to admit I find SSR a bit alien of a topic anyway since I do most of my work on applications that would get no benefit from SSR. This will obviously be a gradual thing but I could use some help understanding the goalposts on more complicated interactions.

I hit a major snag that requires rethinking large part of the solution. JSX executes inside out. Most libraries manage Hydration by creating that Virtual tree first. The problem with inside out without a virtual tree is you don't know who the parent is. You can keep track through execution context but this is JavaScript. You can hoist, you can do whatever you want. Any smart compiler attempt could never leave its compilation scope. What if the Component lives in a different file. More so what I'm getting at is without a Virtual DOM to control the creation of elements I can't guaranteed things are created in an order I can keep track of so how do I know which element I should be hydrating. This one reason libraries like this typically don't use JSX, like Svelte. It's too flexible. Even libraries with tagged template literals like lit-html aren't going to let you insert random data and keep things to DOM. And they have the advantage of controlling all template points explicitly with a function call. The end user can indicate the behavior of that tag where in JSX land it's all supposed to just work as part of the whole solution.

But I had a cool idea that I'm going to work on. See 2 passes are key to know the structure upfront as we go to create the elements, but aren't we already doing 2 passes? Once on the server, once on the client. While I can't solve this through compilation, I can communicate the connection between code and DOM structure from the server. I'm thinking of using keyed attributes that can be read on the client to form a lookup to allow out of order rehydration as the normal client code runs. In this way as long as execution is predictable (It should be) we can hydrate without worrying about all the JavaScript that goes into constructing it. I need to vet this approach but it seems more beneficial than trying to codegen vastly different approach to rendering just to solve hydration.

Ok Good News. This approach seems to be working well. I haven't created a full demo project and it's lacking lots of features but the basis for Server Side Rendering and Client Side rendering are released in beta form in Solid v0.12.0. Until I write a better guide refer to: https://github.com/ryansolid/solid/blob/master/documentation/rendering.md#server-side-rendering-experimental for more details.

I made a small example of how the client-side works to rehydrate. https://codesandbox.io/s/solid-hydration-ri3jn. I think most of the more interesting parts are going to be around SSR patterns/best practices for making apps. I'm not sure where that sits, especially as I have no experience in this area and it seems extensive. As opportunities arise I will try to take this further, but now that the mechanism is there I'm going to shift my focus elsewhere for the time being.

Was this page helpful?
0 / 5 - 0 ratings