DEV Community

Cover image for 5 Places SolidJS is not the Best
Ryan Carniato for This is Learning

Posted on

5 Places SolidJS is not the Best

In this article, I wish to finally address the question "What is Solid not as good at?" The list might not be very long but it exists. No tool is the absolute best at everything. I think it is even more important to understand the shortcomings of our tool of choice than their strengths.

I spend a decent amount of time talking about the benefits of fine-grained reactivity and the power of wielding it in a purist way in Solid. But this doesn't come without trade-offs.

There are certain places where being fine-grained or reactive is not beneficial. Solid fully embracing these, unlike more hybrid solutions like Vue, has slightly different challenges.

You can judge for yourself if these are large enough deterrents.

1. The smallest in small apps

That honor goes to Svelte in trivial examples. Svelte has a very small runtime. They accomplish this by using a reactive system that doesn't need to manage subscriptions. Through compilation Svelte basically wires the reactions directly to the setters. This leads to more component code but has an incredibly small baseline.

While this means Svelte will quickly become larger than Solid as our trivial demo grows, a small VDOM library like HyperApp will be smaller for basic apps. A minimal VDOM library won't be much more than a 200 line patch function. This combined with small components is difficult to match.

Where granular reactivity makes a comeback is as things grow past this. As any library adds features their code grows. A minimal VDOM library does not stay minimal. Reactive libraries tend to re-use their primitives even for the library code. So while maybe the smallest baseline might be 500 lines of code, features tend to scale better.

Still, even if Solid has the smallest Realworld Demo and Hackernew clone, it will never have the smallest TodoMVC. 5.9kb minified + gzipped, might come in under Preact at 6.4kb, but will never touch Svelte at 4.0kb or HyperApp 4.9kb

2. Component Lifecycles

Ever wonder how Svelte is so good at animations. It builds these stages right into the component lifecycle. Look at what happens with a simple interactive hello world:

<script>
  let name = 'world';
</script>

<h1 on:click={() => name = "Jo"}>Hello {name}!</h1>
Enter fullscreen mode Exit fullscreen mode

Svelte compiles this to:

/* App.svelte generated by Svelte v3.32.3 */
import {
  SvelteComponent,
  append,
  detach,
  element,
  init,
  insert,
  listen,
  noop,
  safe_not_equal,
  set_data,
  text,
} from "svelte/internal";

function create_fragment(ctx) {
  let h1;
  let t0;
  let t1;
  let mounted;
  let dispose;

  return {
    c() {
      h1 = element("h1");
      t0 = text("Hello ");
      t1 = text(/*name*/ ctx[0]);
    },
    m(target, anchor) {
      insert(target, h1, anchor);
      append(h1, t0);
      append(h1, t1);

      if (!mounted) {
        dispose = listen(h1, "click", /*click_handler*/ ctx[1]);
        mounted = true;
      }
    },
    p(ctx, [dirty]) {
      if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
    },
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(h1);
      mounted = false;
      dispose();
    },
  };
}

function instance($$self, $$props, $$invalidate) {
  let name = "world";
  const click_handler = () => $$invalidate(0, (name = "Jo"));
  return [name, click_handler];
}

class App extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, instance, create_fragment, safe_not_equal, {});
  }
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Notice that in the fragment it returns an object with a bunch of keys like c, m, p, etc... These are separate hooks into different lifecycles. You can see the lifecycle code right there. Svelte uses a SvelteComponent base class to define its component instance making it easy to manage.

Most VDOM libraries don't even have this ability to slice apart their render/update cycle. Solid by contrast has even less. The equivalent example:

import { createSignal } from 'solid-js';

function App() {
  const [name, setName] = createSignal("world");

  return (
    <h1 onClick={() => setName("Jo")}>
      Hello {name()}
    </h1>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

becomes:

import { template, delegateEvents, insert } from "solid-js/web";
import { createSignal } from "solid-js";

const _tmpl$ = template(`<h1>Hello </h1>`, 2);

function App() {
  const [name, setName] = createSignal("world");
  const _el$ = _tmpl$.cloneNode(true);
  _el$.$$click = () => setName("Jo");
  insert(_el$, name, null);
  return _el$;
}

export default App;

delegateEvents(["click"]);
Enter fullscreen mode Exit fullscreen mode

Our App is just a function call. There is no lifecycle here. We just have a single code path that clones our template and wires up an event and the content insertion. This is incredible for performance as Solid basically generates the same code someone handwriting Vanilla JavaScript with a reactive system would. This means almost zero overhead but there is no Component instance to tap into.

In Solid, all rendering and DOM interaction is a side effect of the reactive system. Even basics like mount/unmount are not based on DOM action. A reactive context is "mounted" when it has finished settling for the first time, and "unmounted" when its parent context is re-evaluated or removed.

This means that while it's possible to handle things like exit animations in a similar way to React or Vue using the reactive lifecycle the framework doesn't really keep track of the DOM in a way that is accessible outside of a user's own references.

3. Deep View Diffing

Fine-grained reactivity is not a diff engine. Any reactive system can be built to do diffing, but Solid uses granularity to avoid doing diffing. This is a large part of why it is so performant. Solid does less diffing than basically every other library out there.

For most things, you don't need to do view diffing. It just isn't necessary because the source of change can know what changed. For other scenarios, Solid has the means through a reconcile helper to diff the data rather than the view. This approach requires a bit more consideration for those cases but benchmarks have shown Solid to perform in line with the most performant VDOM libraries even in the tests catering to them.

But there are a few places where diffing actually is valuable and it becomes much harder in this scenario. Hot Module Replacement(HMR) is one. Where when doing Hydration during Server Side Rendering you expect the browser to reflect what was rendered on the server, for HMR it is a completely different sort of hydration. Things are being changed intentionally by the developer and the boundaries need to be at the module level. We don't get to make things more granular.

Preserving state in a reactive graph that treats the whole render experience (DOM and Components) as side effects makes it really challenging. Not to mention it is a dependency graph so any replacement of reactive nodes needs to propagate down the whole graph. It isn't hard to swap out branches but in place, swaps are very challenging.

React makes use of diffing for apply patches from its concurrent rendering and for its upcoming Server Components too. These aren't the only way to handle the problem. Solid's concurrent rendering uses a sort of quantum approach where each reactive atom used in a transition forks as the change progresses. Synchronous updates that occur between transition start and commit are then applied to each observed reality.

But React definitely has the simplicity advantage for these sort of problems when it can write and throw away at will knowing it can always diff the results.

4. Debugging

This one might just be a needs improvement and not a fundamental strike but I do believe the declarative nature of a library can make it harder to debug. While it is easier to reason about the way change occurs is more distributed.

Picture debugging HTML. One simply doesn't. A declarative system has relations that hold or don't. The execution of that code is queued up and no longer resembles the structure that it describes. Stepping through the engine has already lost track of why that work is queued.

But bugs do happen in user-written Solid code. There are number of imperative sections. Places where we accidentally wire up infinite loops or access reactive properties outside of reactive tracking.

Now the templates themselves are easy to debug as Solid puts all the DOM operations out in the open for the most part so you can see any issues. But this isn't where most bugs occur.

Visualization ultimately seems our way out but figuring out the best way isn't the easiest. Solid's components are basically non-existent and don't own state. The equivalent ownership hierarchy is based on control flow and is meaningless to end-users. The dependency graph, by comparison, can be huge. So large it provides no help unless look in small sections.

Unfortunately, approaches I've come up with to combat this involves changing the graph to fake components in dev mode, but that further changes the code that runs in development from production. I don't believe this is an impossible problem but it is definitely a difficult one.

5. Server-Side Rendering

Alt Text

Solid currently touts the fastest raw SSR speeds of any JavaScript Framework. With exception of Marko, which has respectable performance, often orders of magnitude faster. But that alone doesn't make much of a difference to your end-users. It will save your compute time in your serverless functions but not much else.

Progressive Rendering(streaming) and Partial Hydration make a much bigger impact. Solid has a form of Progressive Rendering. So that sets it apart from almost every other JavaScript framework today, but Partial Hydration in a natural way is a very difficult problem.

Without resorting to hacks like .server.js etc.. it is very difficult to determine intent. While Solid's explicit APIs allow for the greatest control and raw performance in either the server or browser, it is less analyzable than frameworks that build it into the language. Libraries like Marko or Svelte have the ability to identify statefulness and track how it is used through your whole application.

While we haven't seen a framework successfully pull this off as of yet, nor have an architecture granular enough it can leverage it, it will happen. For now, Solid will just have to be satisfied being the fastest raw renderer in the server and the browser, with the knowledge that it is theoretically possible that other frameworks will outperform it on the server one day.

Conclusion

I think these are the most standout places where Solid's approach provides distinct limitations that aren't easily resolved. Not to say there aren't workarounds, but just these are areas that likely will always be less than ideal for Solid.

In some cases, no solutions today are particularly good so perhaps you shouldn't be concerned. But in others, we are dealing with the framework's fundamental nature.

And that's ok as long as you know what you are in for.

GitHub logo solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.

SolidJS

Build Status Coverage Status

NPM Version Discord Subreddit subscribers

WebsiteAPI DocsFeatures TutorialPlaygroundDiscord

Solid is a declarative JavaScript library for creating user interfaces. Instead of using a Virtual DOM, it compiles its templates to real DOM nodes and updates them with fine-grained reactions. Declare your state and use it throughout your app, and when a piece of state changes, only the code that depends on it will rerun.

At a Glance

import { createSignal } from "solid-js";
import { render } from "solid-js/web";
function Counter() {
  const [count, setCount] = createSignal(0);
  const doubleCount = () => count() * 2;
  
  console.log("The body of the function runs once...");

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {doubleCount(
Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
jon49 profile image
Jon Nyman

Here's my front end framework. I think it is under 2k minified (without being uglified first):

github.com/jon49/MealPlanner/blob/...

Inspired by HTMX and other similar frameworks do the back end first development. These frameworks are good for really simple websites that need just a little bit of spice to make them work a little cleaner. Mine isn't as full featured as HTMX or as extensible and I'm going to clean up the code so it won't be as extensible as it is now. But it is nice because you write your HTML like you would a back end only app and then just add hf-ignore for any forms you don't want the framework to work with. I made the back end super performant so it will feel fast except for any latency. Since many front end developed apps are not optimistic in their API calls it doesn't make sense for them to build in any complexity.

If I had built my app with node or deno I could reuse much of the code and have it work offline too, which was my original plan but I like C# and I like deno more than node but deno isn't quite prime time yet for building apps. But once I write the service worker for it, it should be pretty fast with no latency and will transfer less data. I'm still thinking how I want to do that though. I've actually had the app running fully on the front end with no back end but then I needed to actually deliver something for my wife and just ported it all over to ASP.NET Core. It's been fun playing with all sorts of designs.

Collapse
 
ryansolid profile image
Ryan Carniato

Yeah we're seeing a return of interest in this area again. I've talked about this a bit in my SSR series. There is a logical desire to explore. I also work on backend first framework Marko written in JavaScript, which has lead me to appreciate how nice it is to have that unified single HTML feel.

That being said things are really converging. Frontend frameworks are looking how to unify their backend experience and vice versa. While for small things I'd probably go with whatever works, I strongly recommend anyone looking at this space to understand both sides of the equation. Obviously takes frontend not thinking SPAs are everything. But I've seen a lot of talk from the backend side not appreciating the intricacies of preserving client state. Every time we come back around we know a bit more and look at the problem slightly differently.

Collapse
 
jon49 profile image
Jon Nyman

Also, mine works with no JS too with just a little bit of work on the back end to make it work for both cases. I really like how it has come out. But if I ever needed some more dynamic front end then one of these frameworks would definitely be nice to use!

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Qwik seems to beat this today… perhaps the future framework you were talking about…

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

In raw rendering server and browser, Solid is still significantly faster than Qwik. I think it is theoretically possible to make greater server rendering gains through compilation and restrictive language but still waiting to see this pulled it off.

But yes I'd classify the efforts of Qwik and Marko achieving that more granular hydration, (non hydrating) thing I was hinting at. Working on Marko's next version at the time I already had seen this future and was leaving a breadcrumb. It's been great seeing progress in this area the past year and a half.

And the reason this didn't show up in my top 5 is I don't see this a fundamental obstacle for Solid here. Both in direction of looking into similar things to Marko or Qwik because both are based on leveraging subcomponent updates something Solid excels at, and because using techniques like Partial Hydration + Solid is still getting the best scores in loading tests I've seen. Conceptually I see how this could not hold. We need better tests. But I also want to scrutinize the tradeoffs made to accomplish things like resumability.