Skip to content

Overview

A tiny reactive UI framework. Roughly 6.6 KB minified + gzipped including its sole runtime dependency (@preact/signals-core). 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. There’s no <MyComponent /> notion. Components are plain functions returning JSX.
  • 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.

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 diff: 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-diff (src/diff.ts); 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 diff 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) │
│ → diff(live, template, listParents) │
│ → 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”