npm.io
0.3.0 • Published 14h ago

flatsignals

Licence
MIT
Version
0.3.0
Deps
1
Size
26 kB
Vulns
0
Weekly
0

FlatSignals

FlatSignals is an ultra-fast reactivity library (~0.5 KB) optimized for high-frequency, few-to-many updates.

Why it’s fast:

  • No graph traversals
  • O(1) dynamic dependency management
  • Lazy computations by default

Benchmarks

Note: Benchmarks were run in a controlled environment. Results may vary based on hardware and JavaScript engine. You can reproduce these benchmarks by cloning the repo and running pnpm bench.

1-to-64 fanout
Library Operations/sec vs flatsignals
flatsignals 449,848 baseline
alien-signals 261,082 1.72x slower
@preact/signals 230,899 1.95x slower
@reactively/core 182,403 2.47x slower
@vue/reactivity 152,709 2.95x slower
@maverick-js/signals 137,712 3.27x slower
Angular Signals 98,597 4.56x slower
@solidjs/signals 78,849 5.71x slower
High-frequency updates
Library Operations/sec vs flatsignals
flatsignals 976,139 baseline
alien-signals 502,513 1.94x slower
@preact/signals 492,543 1.98x slower
@reactively/core 438,882 2.22x slower
@maverick-js/signals 343,368 2.84x slower
Angular Signals 241,189 4.05x slower
@vue/reactivity 221,537 4.41x slower
@solidjs/signals 202,492 4.82x slower
Diamond
Library Operations/sec vs flatsignals
flatsignals 4,556,987 baseline
alien-signals 3,028,320 1.50x slower
@preact/signals 2,531,788 1.80x slower
@reactively/core 1,688,053 2.70x slower
Angular Signals 1,614,432 2.82x slower
@vue/reactivity 1,563,865 2.91x slower
@maverick-js/signals 1,407,975 3.24x slower
@solidjs/signals 870,080 5.24x slower

Installation

# npm
npm install flatsignals

# pnpm
pnpm add flatsignals

# yarn
yarn add flatsignals

Usage

import { signal, computed, effect } from "flatsignals";

const counter = signal(0);
const double = computed(() => counter.val * 2);
const log = effect(() => console.log(double.val));

With React

Bypass React's render cycle
import { useRef } from "react";
import {
  useFlatRoot,
  useFlatSignal,
  useFlatEffect,
  useFlatComputed,
} from "flatsignals/react";
import { useFrame } from "react-three-fiber";

// example zero rerenders
function FrameExample() {
  const root = useFlatRoot(false);

  const myRef = useRef(null);
  const myCounter = useFlatSignal(1, root);
  const myCounterDouble = useFlatComputed(() => myCounter.get() * 2, root);

  useFlatEffect(() => {
    const val = myCounterDouble.get();
    if (myRef.current) {
      myRef.current.style.setProperty("--clicks", val);
    }
  }, root);

  useFrame((state) => {
    // updates dirty effects every frame
    root.flush();
  });

  return (
    <div>
      <div ref={myRef}></div>
      <button onClick={() => myCounter.set((prev) => prev + 1)}></button>
    </div>
  );
}
Sync with React's render cycle
import { useSyncFlatSignal, useSyncFlatReader } from "flatsignals/react";
import { counter, double } from "./signals";

function MyCounter() {
  const [val, setVal] = useSyncFlatSignal(counter);
  return <button onClick={() => setVal(val + 1)}>Count: {val}</button>;
}

function ReadDouble() {
  const val = useSyncFlatReader(double);
  return <div>{val}</div>;
}

Use Case

Best suited for:

  • High frequency updates that bypasses React's render cycle (e.g., animations, dragging).
  • Granular state tracking to prevent unnecessary parent-to-child re-render cascades.
  • Dynamic dependency graphs that require evaluation every frame (e.g., canvas rendering, node-based editors).

Limitations:

  • Signal limit: Maximum of 32 signals per root. Beyond this limit, effects may trigger even when their tracked signals haven't changed.
  • Set complexity: O(N) time proportional to dependent computations (amortized through batching)
  • Eager propagation: All downstream nodes marked dirty immediately, even when intermediate values unchanged