← Back to DevBytes

SolidJS Performance: Optimization Techniques and Benchmarks

SolidJS Performance: Optimization Techniques and Benchmarks

SolidJS has earned a reputation as one of the fastest JavaScript UI frameworks available today. Unlike virtual DOM-based frameworks, SolidJS compiles templates to real DOM nodes and uses fine-grained reactivity to update only what changes. However, even with these architectural advantages, developers must understand how to leverage SolidJS's primitives correctly to extract maximum performance. This tutorial explores what makes SolidJS fast, why optimization matters, and how to apply proven techniques in real applications.

What Makes SolidJS Fast

SolidJS avoids the virtual DOM entirely. Instead of diffing trees on every state change, it tracks dependencies at the signal level. When a signal updates, only the specific DOM node or expression bound to that signal re-runs. This fine-grained reactivity means updates are O(1) relative to the size of the application, not O(n) like diff-based frameworks.

Key architectural pillars include:

Why Performance Optimization Matters

Even with SolidJS's efficient core, poorly structured reactivity can cause unnecessary recomputations, memory leaks, or janky rendering in large applications. Optimization matters because:

Understanding when and how to optimize ensures your application remains responsive as it scales.

How to Use SolidJS Optimization Techniques

1. Prefer Signals Over Stores for Primitive State

Signals are the lightest reactive primitive in SolidJS. For simple primitive values like booleans, numbers, or strings, signals outperform stores because they have fewer moving parts.

import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clicked {count()} times
    </button>
  );
}

Use stores only when dealing with nested objects or arrays where granular updates are beneficial.

2. Use createMemo for Expensive Derived Computations

When a computed value depends on signals and is expensive to calculate, wrap it in createMemo. This caches the result and only recomputes when dependencies change.

import { createSignal, createMemo } from "solid-js";

function FilteredList() {
  const [items, setItems] = createSignal(Array.from({ length: 10000 }, (_, i) => i));
  const [query, setQuery] = createSignal("");

  const filtered = createMemo(() => {
    const q = query().toLowerCase();
    return items().filter(item => String(item).includes(q));
  });

  return (
    <>
      <input
        type="text"
        value={query()}
        onInput={e => setQuery(e.currentTarget.value)}
      />
      <ul>
        {filtered().map(item => <li>{item}</li>)}
      </ul>
    </>
  );
}

Without createMemo, the filter would re-run on every signal read in the component, not just when items or query change.

3. Use For Instead of Index or Map for Lists

SolidJS provides the <For> component for efficient list rendering. Unlike .map(), <For> tracks items by reference and only updates the DOM nodes for items that actually change.

import { For, createSignal } from "solid-js";

function TodoList() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "Learn SolidJS" },
    { id: 2, text: "Build an app" },
  ]);

  return (
    <ul>
      <For each={todos()}>
        {(todo) => <li>{todo.text}</li>}
      </For>
    </ul>
  );
}

Avoid <Index> unless you specifically need to track by index position, as it re-renders items when the array order changes.

4. Virtualize Large Lists

For lists with thousands of items, use a virtualization library like solid-virtual to render only visible items.

import { createVirtualizer } from "@tanstack/solid-virtual";
import { For } from "solid-js";

function HugeList() {
  let parentRef;
  const rowVirtualizer = createVirtualizer({
    count: 100000,
    getScrollElement: () => parentRef,
    estimateSize: () => 40,
  });

  return (
    <div
      ref={parentRef}
      style={{ height: "500px", overflow: "auto" }}
    >
      <div
        style={{
          height: `${rowVirtualizer.getTotalSize()}px`,
          position: "relative",
        }}
      >
        <For each={rowVirtualizer.getVirtualItems()}>
          {(virtualItem) => (
            <div
              style={{
                position: "absolute",
                top: `${virtualItem.start}px`,
                height: `${virtualItem.size}px`,
              }}
            >
              Row {virtualItem.index}
            </div>
          )}
        </For>
      </div>
    </div>
  );
}

5. Batch Updates with Stores

SolidJS stores allow fine-grained updates to nested data. When updating multiple properties, use the producer function form to batch changes into a single update cycle.

import { createStore } from "solid-js/store";

function UserProfile() {
  const [user, setUser] = createStore({
    name: "Alice",
    age: 30,
    address: { city: "Berlin", zip: "10115" },
  });

  const updateProfile = () => {
    setUser("name", "Bob");
    setUser("age", 31);
    setUser("address", "city", "Munich");
  };

  return (
    <div>
      <p>{user.name}, {user.age}, {user.address.city}</p>
      <button onClick={updateProfile}>Update</button>
    </div>
  );
}

6. Use on() to Control Effect Dependencies

By default, effects track every signal read inside them. Use the on helper to explicitly declare dependencies and prevent unintended tracking.

import { createSignal, createEffect, on } from "solid-js";

function Logger() {
  const [a, setA] = createSignal(0);
  const [b, setB] = createSignal(0);

  // Only runs when `a` changes, even though `b` is read inside
  createEffect(on(a, () => {
    console.log("a changed:", a(), "b is:", b());
  }));

  return (
    <>
      <button onClick={() => setA(a() + 1)}>Inc A</button>
      <button onClick={() => setB(b() + 1)}>Inc B</button>
    </>
  );
}

7. Defer Non-Critical Effects

Use createDeferred to delay expensive computations until the browser is idle, keeping the UI responsive during rapid updates.

import { createSignal, createDeferred } from "solid-js";

function SearchResults() {
  const [query, setQuery] = createSignal("");
  const deferredQuery = createDeferred(query, { timeoutMs: 100 });

  return (
    <>
      <input onInput={e => setQuery(e.currentTarget.value)} />
      <ExpensiveResultsView query={deferredQuery()} />
    </>
  );
}

Benchmarks: SolidJS vs Other Frameworks

The widely cited JS Framework Benchmark consistently places SolidJS near the top. Key metrics from recent runs include:

These benchmarks demonstrate that SolidJS's architecture delivers real-world performance benefits, not just theoretical improvements.

Best Practices

import { lazy, Suspense } from "solid-js";

const HeavyChart = lazy(() => import("./HeavyChart"));

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <HeavyChart />
    </Suspense>
  );
}

Conclusion

SolidJS provides an exceptional performance foundation through fine-grained reactivity and compiled templates, but achieving optimal results still requires deliberate engineering. By choosing the right reactive primitives, memoizing expensive computations, virtualizing large lists, batching store updates, and controlling effect dependencies, you can build applications that remain fast and responsive at any scale. Combine these techniques with regular profiling and benchmarking, and SolidJS will reward you with some of the best performance available in the modern frontend ecosystem.

— Ad —

Google AdSense will appear here after approval

← Back to all articles