Solid.js vs. Next.js

Compare a To Do App built with Solid.js and Next.js.

Introduction

In the previous two articles, we built a basic To Do App with Solid.js, and a basic To Do App with Next.js. In this article, we'll compare several differences, as well as do a performance benchmark by rendering 10,000 objects.

Disclaimers

This article is for entertainment purposes only.

This article is, more accurately, comparing React with Solid.js, as we are not using any Next.js specific features in this comparison.

Next.js was mainly used to scaffold the initial React app, as it is now the recommended framework for building new React apps.

Where to initialize state

In Solid.js, state, also known as "signals" in Solid terminology, can be initialized in the top level scope.

const [todos, { mutate }] = createResource(fetchTodos);

In React, state must be initialized in the component scope.

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>();
}

Achieving this kind of "global" scope with React requires the use of React Context or external state management libraries.

Where to put functions

In Solid.js, functions that mutate state can also live in the top level scope.

async function handleClick(todo: Todo) {
  mutate((prev) => {
    const idx = prev!.findIndex((t) => t.id === todo.id);
    const newState = [...prev!];
    newState.splice(idx, 1, { ...todo, completed: !todo.completed });
    return newState;
  });
  const response = await fetch(`${URL}/todos/${todo.id}`, {
    method: "PATCH",
    body: JSON.stringify({
      completed: !todo.completed,
    }),
  });
  return await response.json();
}

In React, functions that mutate component state is typically defined in the component function.

export default function Home() {
  async function handleClick(todo: Todo) {
    const idx = todos!.findIndex((t) => t.id === todo.id);
    const newState = [...todos!];
    newState.splice(idx, 1, { ...todo, completed: !todo.completed });
    setTodos(newState);
    const response = await fetch(`${URL}/todos/${todo.id}`, {
      method: "PATCH",
      body: JSON.stringify({
        completed: !todo.completed,
      }),
    });
    return await response.json();
  }
}

Note: Most of the state mutation logic is the same between Solid.js and React. It would be even more similar had I used createSignal instead of createResource with Solid.js.

createResource is more of a shortcut for fetching data and setting a signal.

Control Flow

In Solid.js, a loop is achieved with a <For> component and conditional rendering is achieved with a <Show> component.

function App() {
  return (
    <div>
      <h1>Solid.js TODO</h1>
      <Show when={!todos.loading} fallback={<div>Loading...</div>}>
        <button onClick={handleCreate}>Create New To Do</button>
        <button onClick={addTenThousandToDos}>Add 10,000 To Dos</button>
        <ul class="todo-list">
          <For each={todos()}>
            {(todo) => (
              <li>
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onChange={() => handleClick(todo)}
                />{" "}
                <input
                  type="text"
                  value={todo.title}
                  onInput={(e) => handleTitleChange(e, todo)}
                />
                <button onClick={() => handleDelete(todo)}>delete</button>
              </li>
            )}
          </For>
        </ul>
      </Show>
    </div>
  );
}

In React, a loop is achieved with a map function and conditional rendering is achieved with an early return statement.

if (!todos) {
  return <div>Loading...</div>;
}

return (
  <div>
    <h1>Next.js TODO</h1>
    {todos && (
      <div>
        <button onClick={handleCreate}>Create New To Do</button>
        <button onClick={addTenThousandTodos}>Add 10,000 To Dos</button>
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => handleClick(todo)}
              />{" "}
              <input
                type="text"
                defaultValue={todo.title}
                onChange={(e) => handleTitleChange(e, todo)}
              />
              <button onClick={() => handleDelete(todo)}>delete</button>
            </li>
          ))}
        </ul>
      </div>
    )}
  </div>
);

Performance

Chrome devtools was used in this performance test.

10,000 To Do objects were added to the page, while being recorded by the profiling tool.

These were the results:

Solid.js

Solid.js benchmark

Next.js

Next.js benchmark

Solid.js and Next.js performed comparably on Rendering, Painting, System, and Idle time.

However, Solid.js appears to have substantial advantage in Scripting time.

What is the explanation for this?

From the Solid.js Home Page:

Fine-grained reactivity lets you do more with less. Solid is built with efficient reactive primitives you can use from your business logic to your JSX views. This unlocks complete control over what gets updated and when, even at the DOM binding level. With no Virtual DOM or extensive diffing, the framework never does more work than you want it to.

Conclusion

Solid.js comes with a minimal learning curve if you're already solid with React.

Solid.js seems to gain a performance edge by eliminating the necessity for a virtual DOM.

While React/Next.js has a more mature ecosystem, Solid.js appears to be a promising, cutting-edge technology.