It's been a while since I wrote a piece about a SolidJS technology innovation. It's been two years now since we added Suspense on the server with Streaming SSR. And even longer to go back to when we first introduced Suspense for data fetching and concurrent rendering back in 2019.
While React had introduced these concepts, implementing them for a fine-grained reactive system was a whole other sort of beast. Requiring a little imagination and completely different solutions that avoided diffing.
And that is a similar feeling to the exploration we've been doing recently. Inspired equal parts from React Server Components and Island solutions like Marko and Astro, Solid has made it's first steps into Partial Hydration. (comparison at the bottom)
SolidStart
Since releasing Solid 1.0 I've been kinda swamped. Between keeping open issues down and trying to check off more boxes for adoption I definitely have felt spread thin. Everything pointed to the need for a SSR meta-framework, an effort I started even before the 1.0 release.
The community stepped up to help. But ultimately, for getting the beta out the door I would become the blocker. And Nikhil Saraf, never one to sit still, having recently been introduced to Fresh wanted to see if he couldn't just add Islands to SolidStart.
Wanting to keep things focused on a release, I agreed but told him to time-box it as I'd need his help the next day. The next day he showed me a demo where he did not only add Islands, recreating the Fresh experience, but he had added client-side routing.
Accidental Islands
Now the demo was rough, but it was impressive. He'd taken one of my Hackernews demos and re-implemented the recursive Islands. What are recursive Islands.. that's when you project Islands in Islands:
function MyServerComponent(props) {
return <>{ props.data &&
<MyClientIsland>
<MyServerComponent data={props.data.childData} />
</MyClientIsland>
}</>
}
Why would you want this? It would be nice to wrap server rendered content with interactivity without completely losing our low JavaScript for the whole subtree.
However, there is a rule with Islands that you cannot import and use Server only components in them. The reason is you don't want the client to be able to pass state to them. Why? Well if the client could pass state to them then they'd need to be able to update and since the idea is to not send this JavaScript to the browser this wouldn't work. Luckily props.children
enforces this boundary pretty well. (Assuming you disallow passing render functions/render props across Island boundaries).
function MyClientIsland() {
const [state, setState] = createSignal();
// can't pass props to the children
return <div>{props.children}</div>
}
How was he able to make this demo in such short order? Well, it was by chance. Solid's hydration works off of matching hierarchical IDs to templates instantiated in the DOM. They look something like this:
<div data-hk="0-0-1-0-2" />
Each template increments a count and each nested component adds another digit. This is essential for our single-pass hydration. After all JSX can be created in any order and Suspense boundaries resolved at any time.
But at a given depth all ids will be assigned in the same order client or server.
function Component() {
const anotherDiv = <div data-hk="1" />
return <div data-hk="2">{anotherDiv}</div>
}
// output
<div data-hk="2">
<div data-hk="1" />
</div>
Additionally, I had added a <NoHydration>
component to suppress these IDs so that we could skip hydrating assets like links and stylesheets in the head. Things that only ran on the server and didn't need to run in the browser.
And also unrelated, working on the Solid integration with Astro, I had added a mechanism to set a prefix for hydration roots to prevent the duplication of these IDs for unrelated islands.
It just never occurred to me that we could feed our own IDs in as the prefix. And since it would just append on the end we could hydrate a Server rendered Solid page starting at any point on the page. With <NoHydration>
we could stop hydrating at any point to isolate the children as server-only.
Hybrid Routing
For all the benefits of Islands and Partial Hydration, to not ship all the JavaScript, you need to not require that code in the browser. The moment you need to client render pages you need all the code to render the next page.
While Technologies like Turbo have been used to fetch and replace the HTML without fully reloading the page, people have noted this often felt clunky.
But we had an idea a while back that we could take our nested routing and only replace HTML partials. Back in March, Ryan Turnquist(co-creator of Solid Router) made this demo. While not much of a visual demo it proved we could have this sort of functionality with only 1.3kb of JavaScript.
The trick was that through event delegation of click events we could trigger a client router without hydrating the page. From there we could use AJAX to request the next page and pass along the previous page and the server would know from the route definition exactly what nested parts of the page it needed to render. With the returned HTML the client-side router could swap in the content.
Completing the Picture
The original demo was rough, but it showed a lot of promise. It was still had the double data problem for server-only content and this was something we needed to address in the core. So we added detection for when a Solid Resource was created under a server-only portion of the page. We knew that if what would trigger the data fetching could only happen on the server there was no need to serialize it all. Islands already serialized their props passed in.
We also took this opportunity to create a mechanism to pass reactive context through hydrate
calls allowing Context to work in the browser between Islands seperated by server content.
With those in place, we were ready for the recursive Hackernews comments demo:
But there was one thing we were missing. Swapping HTML was all good for new navigations but what about when you need to refresh part of the page? You wouldn't want to lose client state, input focus etc... Nikhil managed a version that did that. But ultimately we ended up using micromorph a light DOM diff written by Nate Moore (of Astro).
And with that, we have ported the Taste movie app demo in its 13kb of JS glory. (Thanks to a gentle nudge from Addy Osmani, and the great work of Nikhil, David, and several members of the Solid community: dev-rb, Muhammad Zaki, Paolo Ricciuti, and others).
The search page especially shows off reloading without losing client state. As you type the input doesn't lose focus even though it needs to update that whole nested panel.
Solid Movies Demo
And on Github
Just to give you an idea of how absurdly small this is. This is the total JavaScript navigating between two movie listings pages, then navigating into a movie in various frameworks with client-side routing from https://tastejs.com/movies/.
Framework | Demo | Size |
---|---|---|
Next | https://next-movies-zeta.vercel.app/ | 190kb |
Nuxt | https://movies.nuxt.space/ | 90.8kb |
Angular | https://angular-movies-a12d3.web.app/ | 121kb |
SvelteKit | https://sveltekit-movies.netlify.app/ | 34.8kb |
Lit | https://lit-movies.netlify.app/ | 108kb |
SolidStart (experimental) | https://solid-movies.app | 13.2kb |
Note: Only the Solid demo is using server rendered partials so it is a bit of an unequal comparison. But the point is to emphasize the difference in size. Other frameworks are working on similar solutions, things like RSCs in Next and Containers in Qwik, but these are the demos that are available today.
Qwik demo was originally part of this but they changed from client navigation(SPA) to server(MPA) which makes it unsuitable for this comparison.
Conclusion
The more apps we build this way, the more excited I am about the technology. It feels like a Single Page App in every way yet it's considerably smaller. Honestly, I surprise myself every time I open the network tab.
We're still working on moving this out of experimental and solidifying the APIs. And there is more room to optimize on the server rendering side, but we think there are all the makings of a new sort of architecture here. And that's pretty cool.
Follow our progress on this feature here.
Top comments (20)
Thanks for clearing that up.
I don't know how many times I've watched the Solid Movies App segment but I wasn't sure I was “getting it”—I had a sense of “islands with dynamically server rendered content” but couldn't quite pin it down if that was the case.
In that regard I suspect that the recent Next.js 13 use of “Server Components” doesn't tell the full RSC story as I was under the impression that in the original December 2020 demo server components were able to send updates even well past the first render into the client's VDOM.
So as I understand it Solid Start's dynamically server rendered islands in the Solid Movies Demo are the functional equivalent to the Server Components in the December 2020 RSC demo—which is why they are being referred to as “Solid Server Components” even though technically Solid's components vanish at runtime.
After the “component (repeated) render function” vs. “component (one time) setup function” confusion I'm afraid you may have set yourself up for having to explain repeatedly that “DOM diffing” doesn't mean that there is a VDOM.
This look solid :)👏👏👏
I'm really impressed! awesome work @ryansolid
The future of solidjs looks bright, I like how you challenged the status quo of the JavaScript Frameworks! this will bring more attention to the possibilities of shipping less Code to achieve interactive web apps.
This is all really exciting and I love your work!
I noticed that backwards navigation in the movies app takes the same time as new navigation. Is this unavoidable? I feel like the ideal solution would have instant backwards navigation.
Yeah it is possible. We haven't done any sort of caching here yet. I always consider caching the last level of optimization. This sort of architecture begs for back/forward caching but haven't created a solution for that as of yet.
Wow, these numbers are really impressive 👏
Thank you for the write up and the detailed explanations.
Something I don't quite understand about nested islands:
Am I missing something here or is this not the answer to the question? Or to put it more directly, can you rephrase why we want nested islands?
You are absolutely right. I've added a bit more of an explanation.
I don't know. I just grabbed the demos that were posted officially and the one from Qwik I had seen in a similar comparison. I haven't seen a Remix one as of yet. I checked their Discord and MJ suggested the community should build one, but it seems to confirm that one doesn't exist currently.
Always a solid knowledge 👍✨💯
I wasted 3 days experimenting with a similar concept using Astro + HTMX, but I haven't realized that Solid Start will support it out of the box. How far are these feature from being completed? I'm extremely impressed with this work!
Still a ways out. I am not content with the DX. And Context is still an unsolved problem. It works fine for these simple things, but there are things it doesn't support and it isn't clear why. I think the direction is good and I'm sold on what it gives, but we need to do more here. More than some linter rules etc.. So we need to spend some more time with it.
isn't something similar to what Iniertiajs is already doing?
inertiajs.com/routing
The protocol suggests that inertia.js is used to build fully client side rendered (CSR) UIs without having to define a separate supporting API (which likely would require lots of client side JS).
The Islands Architecture tries to minimize the shipped JavaScript by fully rendering the page on the server side as HTML and only delivering just enough JS to make the “islands” interactive.
Solid Start goes further by letting the client side islands manage content that was originally rendered on the server and even replace that content with server content rendered at a later time (while all client side components are hydrated on initial page load).
So the goal is to render as much as possible on the server so that the JS for rendering that server rendered content doesn't have to be shipped to the browser.
Thank you for the explanation and the links, really helpful.