npm i @blu3ph4ntom/inval

Layout invalidation, solved.

A dependency graph engine for incremental layout computation.
Not a framework. Not a renderer. The missing primitive.

naive
417K ops/s
inval
4,230K ops/s

The Problem

Every change triggers
a full recompute.

Sidebar resizes, dynamic-height rows, responsive breakpoints — applications have no abstraction for what geometry changed, and what depends on it.

Dashboard lag

Resize one panel. Every widget repaints. No dependency tracking means no optimisation.

Scroll jitter

Dynamic-height rows don't know their own dependencies. Total height recalculates on every scroll event.

Stale layouts

Manual memoization misses dependencies. A breakpoint change silently renders wrong data.

Invisible root cause

No tool surfaces which geometry invalidated and why. Debugging is trial and error.

The Solution

Declare what depends on what.

Inval models your layout as a directed acyclic graph. Inputs feed computed nodes. Change one value — only its descendants recompute.

dashboard.ts TYPESCRIPT
import { input, node, why } from '@blu3ph4ntom/inval'

// Leaf inputs — external world writes here
const width  = input(800)
const height = input(600)

// Computed node — lazy, cached, incremental
const area = node({
  dependsOn: { w: width, h: height },
  compute:   ({ w, h }) => w * h,
})

area.get()       // 480 000 — evaluated once
area.get()       // 480 000 — cached, zero cost

width.set(1000)
area.get()       // 600 000 — only area recomputed

why(area)        // ['area', 'width']

Caching

Returns cached result if no dependency changed. No work done.

Incremental

Only the nodes downstream of what changed recompute — nothing else.

Debuggable

why(node) walks the graph and returns the exact invalidation path.

Zero deps

Pure TypeScript. 12 KB packed. No runtime dependencies, ever.

Features

Everything you need.
Nothing you don't.

Zero dependencies

Pure TypeScript. 12 KB packed. No runtime dependencies, no bundler surprises.

Framework agnostic

React, Vue, Svelte, vanilla JS. Ships as ESM, CJS, and an IIFE for script tags.

Lazy evaluation

Nodes compute only when accessed. Entire subgraphs can stay cold indefinitely.

Precise caching

A result is cached until exactly the inputs it depends on change. No over-invalidation.

Cycle detection

Circular dependencies are caught at construction time with a clear error message.

Debug tooling

why() traces invalidation paths. toDot() exports your graph for visualization.

Batch updates

Group multiple input changes into one atomic operation. One invalidation walk, not many.

TypeScript native

Full generics. InputNode<T>, ComputedNode<T>, all types exported and documented.

71 tests

Diamond dependencies, deep chains, cycle detection, batch semantics — all covered.

Use Cases

Where layout complexity lives.

Virtualized lists

+

Variable-height rows with O(1) scroll updates. Width change recalculates row heights. Scroll change recalculates only the visible range — row heights stay cached.

Resizable dashboards

+

Drag a panel edge — only downstream widgets recompute their dimensions. Zoom changes hit only zoom-dependent nodes. Sidebar resize hits only width-dependent nodes.

Data grids

+

Column resizing, row virtualization, dynamic cell heights — all tracked as a single DAG. Resize column 4, and only the cells and scroll positions downstream update.

Kanban & card boards

+

Column widths, card heights, and scroll offsets form a graph. Add a card — only that column's height and offset recalculate. Other columns stay cached.

Whiteboards & node editors

+

Drag a node — computed edge paths for only that node recalculate. Pan or zoom — bounding box and viewport transforms update incrementally.

Chat & feed UIs

+

Message bubbles have dynamic heights. New messages only recompute from the insertion point down. Scroll lock and anchor positions stay accurate.

Performance

Measured in millions
of operations per second.

Operation Throughput
input.get() — cached 34.6 M ops/s
node.get() — cached 44.3 M ops/s
input.set() + node.get() 1.9 M ops/s
chain depth 10 — set + get 503 K ops/s
diamond (1 → 4 → 1) — set + get 1.5 M ops/s
100 independent chains — set 1, get 1 3.7 M ops/s

Versus a naive approach on 50 widgets with one width change: 417 K vs 4.23 M ops/s — 10× faster.

Start building

Deterministic layouts.
No guesswork.

Drop inval into any project. Zero config, zero dependencies.

npm i @blu3ph4ntom/inval