Client-side Routing without the JavaScript
Ryan Carniato

Ryan Carniato @ryansolid

About: Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.

Location:
San Jose, California
Joined:
Jun 25, 2019

Client-side Routing without the JavaScript

Publish Date: Nov 7 '22
337 20

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

Image description

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

Image description

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>
  }</>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Image description

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/.

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.

Comments 20 total

  • Rafał Goławski
    Rafał GoławskiNov 7, 2022

    Wow, these numbers are really impressive 👏

  • intermundos
    intermundosNov 7, 2022

    This look solid :)👏👏👏

  • docweirdo
    docweirdoNov 7, 2022

    Thank you for the write up and the detailed explanations.

    Something I don't quite understand about nested islands:

    Why would you want this? Well, there is a rule with Islands that you cannot import and use Server only components in them.

    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?

    • Ryan Carniato
      Ryan CarniatoNov 8, 2022

      You are absolutely right. I've added a bit more of an explanation.

  • peerreynders
    peerreyndersNov 8, 2022

    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.

  • Cherif Bouchelaghem
    Cherif BouchelaghemNov 8, 2022

    isn't something similar to what Iniertiajs is already doing?
    inertiajs.com/routing

    • peerreynders
      peerreyndersNov 8, 2022

      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.

  • Yoav Tzfati
    Yoav TzfatiNov 10, 2022

    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.

    • Ryan Carniato
      Ryan CarniatoNov 10, 2022

      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.

  • Madza
    MadzaNov 13, 2022

    Always a solid knowledge 👍✨💯

  • Ryan Carniato
    Ryan CarniatoNov 14, 2022

    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.

  • Josias Schneider
    Josias SchneiderNov 14, 2022

    This is amazing! Great work!

  • Danish Siraj
    Danish SirajNov 15, 2022

    Looks great, definitely worth trying 🤔, great work 👏👏

  • Diego Chavez
    Diego ChavezNov 17, 2022

    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.

  • Michal Czaplinski
    Michal CzaplinskiNov 21, 2022

    Awesome work Ryan!

    Could you elaborate a bit on this:

    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" />
    

    What does the order of the numbers in the data-hk attribute stand for? What would the 2 and the 1 in this string correspond to?

    • Ryan Carniato
      Ryan CarniatoNov 21, 2022

      The order they are created. Since JSX can be inserted in any order we needed to keep track of that. We can't rely on the order things appear in the DOM. Template partials might be hydrated in a different order than they appear in the DOM. So presumably if the 2nd template is created first on the server it will be hydrated first in the client so being able to match that up is critical.

      And the dashes are component depth. That is a bit arbitrary but since our control flow is components it was the easiest way to isolate. It's possible only Suspense and hydration entries need depth but it was a safer bet right now.

  • Oleg Bask
    Oleg BaskDec 25, 2022

    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!

    • Ryan Carniato
      Ryan CarniatoDec 27, 2022

      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.

Add comment