npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-dom-binder

v1.0.1

Published

Blazing-fast, zero-GC CSSOM updater. Bypasses framework diffing by piping raw Float32Array matrices directly to DOM transforms with hardware-level dirty checking.

Readme

@zakkster/lite-dom-binder

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

Zero-GC bridge from flat Float32Array transform matrices to the CSSOM. Bypasses framework diffing.

You have a simulation, ECS, or animation runtime producing 4×4 transform matrices in a typed array. You need them on the DOM. Every framework reconciler — React's virtual DOM, Vue's reactivity, Svelte's stores — wants to allocate, diff, and reschedule. For 1000+ DOM nodes updating per frame, that's measurable GC pressure during the exact moments you can least afford it.

This is the lower path: walk the buffer, dirty-check 16 floats, write el.style.transform only when something actually changed. The shadow buffer is pre-allocated. Stable entities cost zero allocations. Active entities cost one matrix3d(...) string each.

┌────────────────────────────────────────────────────────────────────┐
│                                                                    │
│   ECS / Physics                Float32Array                  DOM   │
│   ────────────                 ────────────                  ───   │
│                                                                    │
│   per frame:        ─────►   [m₀ m₁ m₂ ... m₁₅]   ─────►   el.style│
│                                                                    │
│                              ▼ unrolled diff vs                    │
│                                shadow buffer                       │
│                                                                    │
│                           skip if equal  ──────────►   no write    │
│                           write if changed ────────►   matrix3d(…) │
└────────────────────────────────────────────────────────────────────┘

Why this exists

Frameworks are built around the idea that the renderer owns the schedule: you describe what you want, they figure out when and how. That works beautifully for component trees and event handlers. It falls apart for per-frame transform updates from a simulation source — every useState or reactive set becomes an allocation, a diff, a microtask, a possible re-render of unrelated subtree.

When the source-of-truth is a typed array updated in a hot loop, the right pipeline is: read the buffer, write style.transform, done. No diff, no virtual DOM, no scheduling. This library does exactly that, with a dirty check so you don't pay for stable entities.

The matrix3d(...) form is chosen deliberately: it forces the modern compositor onto the GPU-only update path — no layout, no paint, just composite. One string allocation per changed entity per frame, and that's the entire cost.


Install

npm install @zakkster/lite-dom-binder

ESM only. No runtime dependencies. Browser-only (uses style.transform).


Quick start

import { DOMBinder } from '@zakkster/lite-dom-binder';

// Bind up to 5000 DOM nodes
const binder = new DOMBinder(5000);

// Your DOM — cache references once, never re-query.
const elements = Array.from(document.querySelectorAll('.entity'));

// Your matrix source — typically the output of an ECS or physics step.
const matrixBuffer = new Float32Array(5000 * 16);

// Per-frame loop
function tick() {
    runPhysicsOrECS(matrixBuffer);            // your code
    binder.updateDOMTransforms(elements, matrixBuffer, activeCount);
    requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

That's the whole API surface for normal use.


API

new DOMBinder(maxEntities)

Constructor. Allocates a shadow buffer of maxEntities * 16 floats (~64 bytes per entity). The shadow is NaN-poisoned so the first frame paints every entity unconditionally — guarantees correct initial state regardless of source matrix contents.

| Parameter | Type | Notes | |----------------|----------|-----------------------------------------------------------| | maxEntities | number | Hard upper bound. Coerced via \| 0. Sets buffer capacity. |

updateDOMTransforms(elements, matrixBuffer, count)

The hot path. For each of the first count entities:

  1. Compares 16 matrix components against the shadow buffer.
  2. If anything differs, copies the new matrix into the shadow and writes el.style.transform = matrix3d(...).
  3. If everything is identical, does nothing.

| Parameter | Type | Notes | |----------------|-----------------------------------|------------------------------------------------------| | elements | (HTMLElement \| null \| undefined)[] | Cached node references. Sparse positions are skipped. | | matrixBuffer | Float32Array | Flat 4×4 matrices, stride 16. | | count | number | Active entity count. Clamped against array length and maxEntities. |

invalidate()

Re-poisons the shadow buffer with NaN. Forces every entity to repaint on the next updateDOMTransforms call. Useful after page-level events that may have desynced the DOM from your matrix state — route changes, theme toggles, fullscreen transitions, dynamic style sheet swaps.


Performance characteristics

The hot path per entity:

updateDOMTransforms inner loop (per entity):
    16 × float comparison       ; dirty check, branch-predicted
    if dirty:
        16 × float copy         ; shadow update
        1  × matrix3d string    ; the only allocation
        1  × el.style.transform ; CSSOM write
    else:
        nothing

Memory cost: 64 bytes per entity, allocated once. 10,000 entities = 640 KB. The shadow buffer is the only persistent allocation.

Per-frame cost (steady state): zero allocations for entities that didn't change. One string per entity that did. Your scene's actual GC pressure tracks the dirty count, not the total count.

Why matrix3d and not CSS variables: matrix3d is a single CSSOM property write that the browser routes onto the compositor-only update path on every modern engine. CSS variables require multiple setProperty calls per element and may force style recalc on read. For arbitrary 4×4 matrices, matrix3d is faster.

Why not the Web Animations API: WAAPI is timeline-based — you describe a keyframed animation, the browser owns the schedule. This library is for the inverse case: your simulation owns the schedule, and you need direct per-frame writes. Not a competitor; a complement.


When to reach for this

  • You're building a physics simulation, particle system, or ECS-style game targeting DOM (because of accessibility, document semantics, or hybrid 2D/3D layouts).
  • You have 100+ simultaneously-animated DOM nodes driven by a non-keyframed source.
  • You're integrating a custom animation runtime into a React/Vue/Svelte/Solid app and want the runtime to bypass framework reconciliation entirely.
  • You're rendering a virtualized list or canvas-style timeline where matrix math is the source of truth.

Skip this library if you're animating fewer than ~50 nodes with simple transitions — CSS animations and the Web Animations API are simpler and just as fast at that scale.


Notes & limitations

  • 3D matrices only. The matrix3d(...) form takes 16 components. If your transforms are 2D-only, a sibling library lite-dom-binder-2d could halve the work — open an issue if you need it.
  • No alpha, color, or filter binding. This is strictly transform. If you need opacity or color from the same buffer, write a sister binder; the dirty-check pattern generalizes cleanly.
  • The string allocation is fundamental. el.style.transform is a setter on a CSSOM property; it accepts a string. There's no zero-alloc path through the standard DOM API. CSS Typed OM (.attributeStyleMap) avoids string parsing on the read side but still allocates CSSTransformValue objects on write — generally a wash.
  • Browser-only. The implementation only touches the DOM on the write path; you can run the dirty check in any environment, but the actual style.transform assignment requires a real HTMLElement.

License

MIT. See LICENSE.txt.