Skip to content

Overview

A tiny reactive UI framework. Roughly 11 KB minified + gzipped including its sole runtime dependency (@preact/signals-core); ~12 KB if you also import arraySignal from the optional kerfjs/array-signal subpath. Four primitives:

  • Signals — fine-grained reactive values. signal(), computed(), effect(), batch().
  • Stores — composable testable units of state. defineStore(), resetAllStores().
  • Rendermount(rootEl, () => jsx). JSX renders to a structured SafeHtml; kerf’s segment-aware diff reconciles static surrounds while a keyed list reconciler owns rows from each(...). The split keeps partial-update / select-row / swap-rows costs at O(changes), not O(rows).
  • Event delegationdelegate() (Tier 1, bubble) and delegateCapture() (Tier 2, capture) replace per-element listeners with one root-level listener per event type.

Plus a JSX runtime (kerfjs/jsx-runtime) and an SVG-aware toElement() for direct JSX-to-DOM conversion.

  • Not a component framework. <MyComponent props /> works as JSX sugar — the runtime calls MyComponent(props) and uses the returned JSX — but there’s no per-instance state, no hooks, and no lifecycle. Components are plain functions; state lives in module-scope signals or stores.
  • Not a router. Not a build tool. Not an SSR framework (though SafeHtml.toString() works server-side if you want it).
  • Not opinionated about styling. Bring your own CSS.
  • Not magical. There’s no compiler, no virtual DOM, no scheduler, no concurrent rendering, no hooks model, no lifecycle. The “no compiler” rule is non-negotiable — kerf will not ship an opt-in codegen package either. If you want compile-time fine-grained reactivity, pick Solid; that’s Solid’s value proposition, and Solid does it better than a kerf-compiler ever could. Kerf’s positioning is “the fastest framework that needs no build step beyond your existing one,” which means accepting Solid’s architectural-floor numbers on update-path benchmarks (~6ms select-row, ~20ms partial-update) as the ceiling. The goal is to close the runtime-vs-compiled gap on every benchmark kerf can close without a compiler — not to match Solid on the ones that require one.

kerf is a good fit when:

  • You want fine-grained reactivity without buying into a framework’s full mental model.
  • Your app is server-rendered HTML + interactive islands, and you want a tiny client-side enhancement layer.
  • You care about preserving live DOM identity across re-renders (focus, selection, in-flight pointer interactions, third-party widget instances).
  • Your users include people who run with assistive tech, where DOM identity preservation matters more than it does in benchmark loops.

It’s a poor fit when:

  • You need a full ecosystem of components, routers, devtools, SSR primitives. Use React / Vue / Solid.
  • Your team is heavily invested in a framework’s conventions. The cost of switching outweighs the runtime size win.
  • You want compile-time JSX transforms that produce optimal DOM ops directly. Use Solid.

The runtime answers two questions:

  1. WHEN do we re-render? Answered by signals: an effect() re-runs whenever any signal it read during its last run changes. mount() wraps effect() so that re-renders happen automatically when the JSX you return depends on a signal that changes.

  2. HOW do we re-render? Answered by kerf’s morph: render JSX to a SafeHtml (which is a string for static content and a structured tree where lists or list-containing parents appear), then walk the live DOM in lock-step. Static surrounds go through a general-purpose tree-morph (src/morph.ts, also exported as morph() for one-shot consumer use); list contents go through a keyed reconciler (each(...)’s side of mount) that operates directly on live children — no parse-the-whole-list step. Element identity is preserved wherever the morph matches by key (id, data-key) or position.

Everything else is detail.

user code
─────────────────────────────────────────────
const count = signal(0);
mount(rootEl, () => ( ← effect() wrapper
<div>
<button data-action="inc">+</button> ← Tier 1 delegation target
<span>{count.value}</span> ← signal read tracked
</div>
));
delegate(rootEl, 'click', '[data-action="inc"]', () => {
count.value += 1; ← signal write triggers re-run
});
─────────────────────────────────────────────
│ count.value++
┌─────────────────────────────────────────┐
│ effect() fires the render fn │
│ → SafeHtml (segment tree) │
│ → morph(live, template, ownedItems) │
│ → each() reconciler patches each list │
│ → minimal DOM mutations applied │
└─────────────────────────────────────────┘

1.6 Reading order for the rest of the docs

Section titled “1.6 Reading order for the rest of the docs”