Skip to content

06 — Fast signals (createSignal)

use() has a small fixed cost — ~550 ns per call at runtime (hooks bookkeeping, devtools, lifecycle). For hot loops, animation frames, or hundreds of signals per component, drop to createSignal — the escape hatch added in Phase 1. Per-call cost: ~22 ns.

tsx
import { createSignal } from 'flexium/core'
import { unsafeEffect } from 'flexium/core'

// NOT a hook — call at module scope or anywhere
const [count, setCount] = createSignal(0)

console.log(count())     // 0   ← getter is a function call
setCount(1)
console.log(count())     // 1

// Subscribe with unsafeEffect
unsafeEffect(() => {
  console.log('count is now', count())
})

When to use

Use caseAPI
React-style component hookuse(0) — value form, [value, setter]
Hot loop, animation, 100+ signals/componentcreateSignal(0) — getter form, [getter, setter]
Plain async/non-React codecreateSignal(0) — works anywhere

Compile-time substitution (Phase 7)

If you build with vite-plugin-flexium and optimize: 'auto', safe use() call sites are automatically rewritten to createSignal at build time:

tsx
// Source (you write):
function Counter() {
  const [count, setCount] = use(0)
  return <button onclick={() => setCount(c => c + 1)}>{count}</button>
}

// Compiled (Babel AST pass — invisible to you):
function Counter() {
  const __sig0 = _createOwnedSignal(0)
  const count = __sig0[0]()
  const setCount = __sig0[1]
  return <button onclick={() => setCount(c => c + 1)}>{count}</button>
}

Result: per-call cost drops from 550 ns → 13 ns with zero code change. The compiler skips use(asyncFn), use(value, { key }), and conditional/loop usage (those fall back to the runtime path).

Coverage on real-world apps: ~70% of use(value) call sites transformed automatically.

Owner lifecycle

createSignal outside a component is a permanent module-level signal — no cleanup. Inside a component, prefer use(0) so cleanup runs on unmount.

Compile-time substitution uses _createOwnedSignal internally, which ties cleanup to the component via Phase 4's ComponentWeakRegistry (WeakRef + FinalizationRegistry). You don't have to think about it.

Computed

The functional form of createSignal's sibling:

tsx
import { createSignal, createComputed } from 'flexium/core'

const [count, setCount] = createSignal(0)
const doubled = createComputed(() => count() * 2)

console.log(doubled())   // 0
setCount(5)
console.log(doubled())   // 10

Benchmarks

Measured on Apple M4, Node 24:

Operationuse()createSignal()Speedup
Create signal14,000 ns22 ns636×
Write (no subscriber)167 ns7.3 ns23×
Update + sync end-to-end14,010 ns387 ns36×
Effect creation14,000 ns80 ns175×

The escape hatch beats Solid.js's createSignal (~800 ns end-to-end).

API surface used

  • createSignal(initial) — getter-form signal [getter, setter]
  • createComputed(fn) — getter-form derived signal
  • createEffect(fn) — getter-form effect
  • unsafeEffect(fn) — same as createEffect (auto-disposed if inside component)

Next

Shared state across componentsuse(initial, { key }).

Released under the MIT License.