One of the most interesting JavaScript frameworks gaining traction these days is SolidJS. Solid is interesting because it takes JSX (React’s templating language) in novel directions. It decorates JSX with a handful of reactive primitives. It is compiled (similar to Svelte). It layers on higher-order services (like a central store and events). And for good measure, it tosses in full-featured SSR (server-side rendering) and Suspense.
I talked to Solid’s creator, Ryan Carniato, about how he and the Solid team pulled this off, how Solid relates to industry developments, how innovative front-end features (like partial hydration, streaming SSR, and Suspense) are implemented, and what motivates him to keep pushing the limits of front-end JavaScript performance.
Matthew Tyson: Hey Ryan, thanks for taking the time to chat today. First question, Solid is a compiled framework, like Svelte. Were you inspired by Svelte to go the compiled route?
Ryan Carniato: No. Svelte didn’t go hardcore compiled until V3 in 2019. We already had this system and were crushing the benchmarks as of early 2018. Svelte has some cool ideas that take compilation further and I was aware of them in terms of some decisions but not the initial direction.
Tyson: I think the amount of innovation in front-end JavaScript is astounding. Do you have a sense of where we are in that evolution? Are we near a peak?
Carniato: I think that we have hit the limits of what we can do in terms of raw rendering performance and honestly it isn’t good enough. That’s why this area keeps evolving, we know we can do better. It’s also why you see so much research into other places to get every last ounce of performance or to improve user experience.
I’ve been saying this for a couple years. This isn’t slowing down. There is technology being used in some frameworks and libraries that hasn’t been tapped in some of the more popular ones and the stuff continues to be worked on and improved. I understand the desire for things to level out and in some sense it has as JS frameworks reach more maturity, but the target is still moving.
So there is much more to be done.
Tyson: You have put a lot of research and thought into performance. Solid reflects that (as seen in the benchmarks), but Solid also strikes me as having a very clean dev experience. Is that a focus for the Solid team, or is that more of a happy side effect?
Carniato: Well, I started with Knockout because I liked the DX [developer experience]. I liked the control. But at the same I saw all the flaws that React’s early press highlighted. So I looked at how we could fix it. I didn’t go out of my way since the formula was there. I was more focused on proving the performance. But the characteristics that made Knockout strong, like composable primitives and declarative data patterns, just continued to have a positive effect on DX.
Instead of having to invent new abstractions I just kept on finding I had the tools already. This kept things simpler in a sense. So I guess both? I started from DX and wanted to land on performance and consolidated on DX.
Tyson: I feel that the way the framework takes a few reactive primitives and layers on top of them makes it easy to work with, mentally. Solid also delivers SSR (server-side rendering). That continues to be an area of expansion. I know it’s a technical challenge to pull off. Has isomorphic support been part of your vision from the beginning? What was your experience in doing it?
Carniato: Hah... not at all. I was very much on client rendering is faster than server rendering. And to be fair the way I was doing it, it was. When frameworks out there wait for async data to respond (like you found with Next, Nuxt, or Sapper) client rendering even can beat it at paint and TTI [time to interactive]. I was putting out things like the Realworld Demo and was content how Solid scored better than these server-rendered implementations. But this is only true on fast networks.
Over time people would see Solid’s performance and SSR became the most requested feature, not even close. I’d only worked on interactive, behind log-in apps, so I had no experience here. Luckily I came across Marko, eBay’s framework, which had pretty much perfected the art of SSR. Solid and Marko have slightly different goals but both projects benefited from each other immensely.
For Solid’s benefit I was exposed to the best techniques of doing SSR. It required rewiring my thinking. But I embraced it figuring out how to compile to the most optimal raw rendering, as well as incorporating streaming SSR. The latter wasn’t too hard given that I already had Suspense, so I just allowed it on the server. It was around that time it became clear to me what React has been up to all these years (and what is coming in React 18 next year). But I was able to get this into Solid last summer. And it’s amazing. This is the future.
Tyson: I need to research Marko.
Carniato: Yeah, people don’t know Marko but when it comes to multi-page apps and server rendering it has been deploying techniques at eBay production levels for the better part of a decade. Other frameworks are just starting to add features that they’ve had for years. Like streaming SSR and partial hydration.
Tyson: Can you describe how hydration works, and why partial hydration is an improvement?
Carniato: It’s the process of running the components on the client for the first time in the browser to get them set up for future updates. We don’t render anything but it generally follows the same top-down execution. So partial hydration is about not shipping all that component code. Less JavaScript to the browser improves page load and interactivity speeds.
Unfortunately you can only really leverage partial hydration in certain types of apps. Our typical single-page apps don’t really get to leverage this as eventually everything needs to be in the browser. It amounts to lazy loading. But in multi-page apps true partial hydration means never sending that JavaScript.
Partial hydration might be a bit off topic, since it requires a different application architecture to leverage and almost no frameworks today optimize for this case. We are just starting to see things like Astro trying their hand at it.
Tyson: Thanks for describing that. Would you mind just describing streaming SSR in a brief way?
Carniato: OK, you’re asking all the tricky questions!
Streaming SSR is based around the idea that we can render as we fetch. If you’ve ever developed a page with async loading in the browser you tend to start rendering your component, and start fetching the data and showing a placeholder. When the data loads you then render the content. All streaming SSR is, is doing the same thing on the server.
When you request your page it starts rendering and fetching that async data. It starts streaming the response back with all the placeholders in place immediately so the client doesn’t wait. However it keeps the response stream open. So when the data finishes loading it can be rendered and sent along the stream. We flush out script tags with it to inform the browser app it’s time to insert and hydrate the new parts as they come in.
So the final result is the synchronous part of the page sent immediately without waiting for async with placeholders very similar to the client loading experience. You see the content load in as available but this happens faster than client rendering since you were able to start the process with the initial server request instead of waiting for the page to be sent to the browser and the JavaScript be requested and processed. But best of all you get to write your apps the same way you always have. We don’t need to hoist out data fetching. It just has the same mental model as the client.
It’s a best of most worlds scenario. And it’s clear to see why React is excited about introducing this in React 18.
Tyson: Wow, thanks for diving into that. I was going to ask about Suspense next. React had to really dig into things to make Suspense work, with a breaking change to a new render engine. Did you guys find it to be a tough thing to implement?
Carniato: Yeah, hah... I went through a couple different versions. I don’t think people realize how not apples-to-apples it is when a framework like Vue or Preact says it has Suspense. Rendering placeholders and new content off-screen is relatively easy. It’s “transitions” and concurrent rendering that make this more challenging. Those frameworks don’t support that.
My first model was really simplistic and was based around just branching realities at each nested control flow. But it required people opting in at each control flow, which was awkward.
My second (and current) approach meant collecting and forking each reactive atom when it was denoted by a transition. And then all future changes either are applied to the transition branch (if they are a transition themselves) or to both if they are regular updates.
This means that while I fork reality there is only one future that is being worked on concurrently. I couldn’t figure out how to crack multiple futures like React had mentioned and I figured it was good enough so I just released it a year ago and went with that. As it turned out, React just announced they too have gone to single future for React 18 so it seems I wasn’t alone finding multiple future realities really difficult to reconcile efficiently.
Luckily Solid was still 0.x and there wasn’t that much in the way of breaking changes. Remember Solid components render once and most communication is through the reactive system granularly. Unless the developer is hoisting out non-reactive values they read from multiple branches the rest is just taken care of by the fact that Solid controls the communication.
Tyson: Gotcha. I feel like we’ve gone from coding to sci-fi with reality forking. I have just a follow-up question to that answer and one last question—thanks so much for your time.
In React, Suspense depends on a data store that implements the API for interacting with the Suspense component. Does Solid do a similar thing via the createResource
function? (I haven’t gotten into Solid’s getResource
just yet.)
Carniato: Yes. The difference is being reactive and not re-rendering. It isn’t so much a cache but a special signal that knows when its data is stale, so that at some later point when it is read we can be sure to inform any Suspense context above it that it needs to do something. (Yes, Solid’s Suspense is implemented with Context API.) But the special primitive is a blessing since it also is used more like a cache on the server as it facilitates auto-serializing of data between client and server for SSR. And it helps us register promises so the server rendering knows when it is done and can close the stream.
Tyson: Awesome. So, you have put an intense amount of work (~800 commits since 2018 on the GitHub project) into Solid. What keeps you motivated?
Ryan Carniato: It’s a funny thing. I don’t know when this changed. Like I decided one day that I wasn’t doing enough dev work at work as I got into more managerial positions and I was up late with my newborn daughter at the time. And I started tinkering and picking it up more at that point. Still just as a novelty for myself on a private repo. Then I found the JS Framework benchmarks and realized the approach [I was taking] could be fast so I open sourced [my project] so I could be included.
For a while that is what motivated me—just shaving time and figuring out how to be the fastest. And then React announced Hooks and everything changed. Honestly I couldn’t believe it. It was like they’d just pointed things Solid’s way. I started writing articles and for the longest time just knowing that we were on to something kept propelling me forward. I still feel that way with the things we haven’t released yet.
Every new problem seems to morph from a weakness to an advantage. It’s an incredible feeling when things seem to work that well. I just can’t picture having things go any other way. I’m hooked (pardon the pun).
Tyson: Yeah, it’s pretty extraordinary how the world seems to keep shifting to vindicate different design choices Solid has made. I know I learned some things. Is there anything you’d like to add?
Carniato: Probably only this. There is a lot of chatter and narratives around this framework vs. that framework and it’s all exaggerated. There are implications to the choices that frameworks and libraries make to be sure. But you always reach the end where you are building a product with the tools you have and the solution is a little messy and perhaps not the most elegant. But there is a reality and you have a deadline to meet.
As much as we like to do theoretical armchair racing and say things are “strictly” better it only goes as far as the developer’s ability to use these tools.
If there is any takeaway from what we’ve accomplished with Solid is that there is still room to explore and that established boundaries and conventions still have room to expand. Work with focus, but when you are on the other side evaluating things don’t be the person who just puts everything in a box and moves on. Solid wouldn’t be here with that mindset, nor will the next big thing.
Tyson: I think that sense of space to explore is very exciting.
Oh, one more question. I’m curious, did Solid’s createSignal
syntax come first, or React’s useState
? (Editor: They both use destructuring assignment.)
Carniato: React’s useState
came first. I was staring at it for so long just hating every permutation. I was looking at:
const count = createSignal(0) count.get(); count.set(value);
But I hated it. I knew I didn’t want to do the following either.
count() count(value);
Nor:
count.value count.value = value;
I thought of doing:
count() count.set()
But it felt too clever. Still I needed a primitive so I was going to go with this.
But the plan was to try to push everyone to use what I call createStore
now as the main primitive. It used to be called createState
after React’s class state. So while I needed signals I figured everyone would just use:
const state = createState({}); state.count; state.set({ count });
But then the second I saw Hooks I was like... this is it! I have no idea why I didn’t think of returning an array. I’d gone through so many permutations.
I saw it and it just worked. Over time people were more drawn to Signals than createState
and it was confusing people coming from React expecting it to be the same as useState
. So I renamed it to createStore
.
Tyson: It’s funny because when I first saw that syntax in React I was like, what the heck is this thing? But it really works well and it’s compact.