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

@lazily-hub/lazily-html

v0.1.0

Published

Isomorphic fine-grained HTML renderer on @lazily-hub/lazily-js. Preact/React-compatible API (Component class, hooks, createElement/h, createContext, forwardRef, memo, render, renderToString) with Solid-style run-once components: setState writes a lazily S

Readme

lazily-html

CI npm

Isomorphic fine-grained HTML renderer on @lazily-hub/lazily-js — browser and server (SSR via renderToString) from one package. Preact/React-compatible API surface (createElement/h, Fragment, Component, hooks, createContext, forwardRef, memo, render, renderToString) with Solid-style run-once components: setState writes a lazily cell and only the DOM expressions that read it patch — no virtual-DOM diffing of the component subtree.

Named lazily-html (not lazily-ui) because the html carves the renderer- target namespace: sibling packages like lazily-slint, lazily-tui, etc. can sit alongside this one as alternative UI surfaces on the same lazily reactive core.

Install

npm install @lazily-hub/lazily-html @lazily-hub/lazily-js

Two renderers, one reactive core, both isomorphic

Both share the same lazily bridge (owner.js) and the same Preact-compat layer (Component, hooks, createContext, forwardRef, memo):

| Import | Renderer | Style | Size | Use when | |---|---|---|---|---| | @lazily-hub/lazily-html (default) | lite | relementjs/VanJS-style (tags.div(...)) + Preact h(), + renderToString SSR | ~4 KB browser / ~2 KB SSR | the common case; tiny widgets, isomorphic, embedded | | @lazily-hub/lazily-html/full | full | dom-expressions hyperscript + Preact h() | ~6.5 KB | big keyed lists (LIS reconcile), event delegation, hydration (full SSR via dom-expressions server.js is a planned follow-up) |

Why two?

The lite renderer is ~150 lines on the lazily bridge, modeled on relementjs (itself a VanJS fork). It detects function-valued props/children and wraps each in a lazily effect that patches a single DOM attribute, text node, or child. It has no keyed-list reconcile and no event delegation — fine for ~90% of UIs, marginal only at thousands of rows.

The full renderer vendors dom-expressions (Ryan Carniato, MIT) — the canonical fine-grained DOM runtime that powers Solid — with its bare rxcore import rewritten to owner.js. That buys keyed LIS list reconcile, document-level event delegation, full SVG/attribute/property tables, custom-element handling, and hydration. Same Preact API on top.

Option-A semantics — read this

This is a fine-grained renderer, not a VDOM renderer. The one semantic deviation from stock Preact/React:

useState / useReducer return a Solid-style accessor as the first element, not a snapshot value.

const [count, setCount] = useState(0);
// count is () => number, NOT number.

Why: in a no-compile hyperscript renderer, the only way JSX {count} can be reactive is if count is a function the renderer detects and binds in an effect. Reading the accessor inside any tracked effect registers the lazily dependency edge, so setCount(v) patches exactly the DOM expressions that read it. The component body runs once per mount (#lzdomnoTrackBody). useSignal is provided as a Solid-familiar alias.

For class components, this.state is a Proxy whose property reads hit lazily cells: this.state.count inside () => this.state.count is reactive, and this.setState({count: v}) patches the bound expressions without re-running render(). this.props auto-unwraps reactive accessor props on read.

Quick start

Lite renderer (default)

import { h, useState, render } from "@lazily-hub/lazily-html";

function Counter() {
  const [count, setCount] = useState(0);
  return h("button",
    { onClick: () => setCount(count() + 1) },
    () => `clicked ${count()} times`,
  );
}

render(h(Counter), document.getElementById("app"));

relementjs-style alternative (same renderer):

import { tags, useState, render } from "@lazily-hub/lazily-html";
const { button } = tags;

function Counter() {
  const [count, setCount] = useState(0);
  return button({ onClick: () => setCount(count() + 1) }, () => `clicked ${count()} times`);
}

Full renderer (dom-expressions-backed)

import { h, useState, render } from "@lazily-hub/lazily-html/full";

function Counter() {
  const [count, setCount] = useState(0);
  return h("button",
    { onClick: () => setCount(count() + 1) },
    () => `clicked ${count()} times`,
  );
}

render(h(Counter), document.getElementById("app"));

The full h is hyper-dom-expressions hyperscript (CSS-selector-capable: h("div.box#main", ...)) and the client export exposes the dom-expressions utilities (insert, spread, template, delegateEvents, For/Show helpers) for Solid-style patterns.

Server-side rendering (isomorphic)

The same component tree renders to an HTML string on the server — no DOM global required. Reactive accessor expressions evaluate to their initial value; effect hooks (useEffect/useLayoutEffect) no-op on the server.

import { h, useState, renderToString } from "@lazily-hub/lazily-html";

function Counter() {
  const [count] = useState(0);
  return h("button", null, `clicked ${count()} times`);  // or () => `...` for the accessor pattern
}

const html = renderToString(h(Counter));
// => "<button>clicked 0 times</button>"

useState / useMemo / useContext / createContext / Component all work identically on browser and server, so SSR output matches the browser's first paint. renderToStaticMarkup is an alias that also enforces the server render mode around the call.

Hydration via lazily sync (resumable, ~526 B)

Hydration here is a sync operation, not a re-execution. The server emits HTML with data-lzh anchor attributes alongside a snapshot of the reactive values at those anchors. The client applies the snapshot to a SyncedView (its reactive state arrives authoritative, not recomputed), then a hyop-shape walker (querySelectorAll('[data-lzh]')) binds each anchor to the synced cell. The component tree never re-runs on the client.

// Server
import { h, renderToStreamable } from "@lazily-hub/lazily-html";
const Counter = () => h("button", { id: "c" }, () => `count: ${0}`);
const { html, snapshot } = renderToStreamable(h(Counter));
// → { html: `<button id="c" data-lzh="a1">count: 0</button>`,
//     snapshot: { version: 1, anchors: { a1: "count: 0" } } }

// Client
import { hydrate, SyncedView } from "@lazily-hub/lazily-html";
const root = document.getElementById("app");
root.innerHTML = html;
const view = new SyncedView();
view.applySnapshot(snapshot);          // authoritative state — no re-derivation
hydrate(root, view);                   // hyop-shape anchor walk (526 B)
// Updates flow straight through the synced graph:
document.getElementById("c").addEventListener("click", () => {
  view.set("a1", `count: ${next()}`);  // patches only this button's text node
});

This repurposes lazily's sync machinery — the SyncedView.applySnapshot / applyDelta / get / set surface mirrors lazily's GraphView.applySnapshot / applyDelta. v0.1 ships static-snapshot hydration: anchor-scoped values, sole-function-child text bindings, and function-valued attribute bindings. The upgrade path is live Delta streaming (same SyncedView.applyDelta API, lazily Delta wire format, capability-filtered via Snapshot.filterReadable) — hydration then becomes "the first sync op," with every subsequent server push a Delta through the same pipe.

Anchor descriptor format (space-separated in data-lzh):

  • <id> → text binding (el.textContent tracks the cell)
  • a:<attr>:<id> → attribute binding (el.setAttribute(attr, ...) tracks the cell)

Architecture

@lazily-hub/lazily-js/reactive  ← lazily Context (Cell/Slot/Signal/Effect)
            │
            ▼
       owner.js                 ← rxcore bridge: root / effect / memo / source / onCleanup
            │
   ┌────────┴────────┐
   ▼                 ▼
 lite.js          full/            ← host-element renderers
 (relementjs)     (dom-expressions client + reconcile + constants + hyper)
   │                 │
   └────────┬────────┘
            ▼
   mount.js + instance.js + component.js + hooks.js + context.js
            ← shared Preact-compat layer (renderer-agnostic)

Size

Generated for package @lazily-hub/lazily-html version 0.1.0. Every entry is minified + brotlied, tree-shaken to the named import (size-limit + esbuild, the same pipeline Webpack/Rollup/Vite apply via "sideEffects": false).

Refresh command:

npm run build            # regenerates this table as part of every build
npm run test:size        # gate: fails CI if any entry exceeds its budget

| Import | Size | Budget | |---|---:|---:| | lite: h + Component + hooks + render | 3.96 KB ✓ | 4.50 KB | | lite: tags (relementjs-style) | 3.29 KB ✓ | 3.80 KB | | server: renderToString (SSR) | 2.17 KB ✓ | 2.50 KB | | hydrate (client) + SyncedView | 543 B ✓ | 1.50 KB | | owner bridge (lazily adapter) | 384 B ✓ | 1.50 KB | | full: h + Component + hooks + render (dom-expressions-backed) | 6.46 KB ✓ | 7.50 KB |

Vendored third-party code

src/full/client.js, src/full/reconcile.js, src/full/constants.js, and src/full/hyper.js are vendored from dom-expressions and hyper-dom-expressions (© Ryan Carniato, MIT). The only change from upstream is rewriting the bare rxcore import to ../owner.js and adding .js extensions for Node ESM. Diff against upstream before upgrading.

License

Apache License 2.0. See LICENSE and NOTICE.

The vendored dom-expressions and hyper-dom-expressions portions remain under their upstream MIT terms; see THIRD_PARTY_NOTICES.