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

dscroll

v1.2.0

Published

Slim, fast, vanilla-JS virtual-scrolling library for fixed-height row lists. Real DOM-node recycling pool, direction-biased buffer with velocity-aware scaling, plugin architecture. Zero dependencies.

Readme

DScroll

A slim, fast, vanilla-JS virtual-scrolling library for fixed-height row lists. Zero dependencies. Plugin-extensible. Real DOM-node recycling pool.

Author: Dharmesh Patel · License: MIT · Version: 1.2.0

DScroll renders only the rows currently visible in a scroll container, plus a small buffer ahead and behind. It's intentionally narrow: fixed-height rows, single-axis (vertical), no framework adapters. Variable heights, multi-column layouts, etc. land later as plugins or in V2.

What makes DScroll different

  • Real DOM-node recycling pool — most vanilla peers destroy + recreate row nodes on every range crossing. DScroll keeps a fixed pool and rebinds existing nodes, paying zero appendChild/removeChild cost during scroll.
  • Direction-biased buffer with velocity-aware scaling and inertia projection — most peers use a fixed symmetric overscan. DScroll renders more rows ahead of motion direction, scales the buffer with scroll velocity, and projects 4 frames forward under inertia (mitigates iOS Safari's blank-on-momentum issue).
  • Plugin architecture where every V2+ feature lands as a plugin without touching core (extend() + scheduleAfter() primitives).
  • Zero allocations in the hot scroll path (steady state).
  • ~605 LOC core across dmath.js + dpool.js + dscroll.js.

Where it's used

DScroll powers the project task panel virtualization in DPlan, where it scrolls many-thousand-task plans on desktop trackpads at 60 fps with a fixed pool of ~225 row nodes.

This package is the standalone extraction of that library — same code, same contract, available for any project that needs vanilla virtual scrolling.


Install

npm install dscroll

Or drop the folder in directly (no build required):

cp -r path/to/dscroll/ ./vendor/dscroll
import DScroll from 'dscroll';
// Or with the vendor copy:
import DScroll from './vendor/dscroll/dscroll.js';

For TypeScript users, declarations ship as dscroll.d.ts alongside.


Quick start

<div id="my-list-scroll" style="height: 600px; overflow-y: auto">
  <div id="my-list-rows" style="position: relative"></div>
</div>

<script type="module">
  import DScroll from 'dscroll';

  const data = [/* …5000 task objects… */];

  const ds = DScroll.create({
    scrollContainer: '#my-list-scroll',
    rowContainer:    '#my-list-rows',
    rowHeight:       28,
    renderRow(node, task, ctx) {
      // Mutate node in place. Called when node binds to new data.
      node.dataset.id   = task.id;
      node.textContent  = task.name;
      node.className    = 'row' + (task.urgent ? ' urgent' : '');
    },
  });

  ds.setData(data);
</script>

That's it. The library handles the rest.


Public API

Creation

DScroll.create(options) → instance

| Option | Type | Required | Description | |---|---|---|---| | scrollContainer | string \| Element | ✓ | Element with overflow-y: scroll. CSS selector or DOM node. | | rowContainer | string \| Element | ✓ | Element inside scrollContainer that holds rows. Must be position: relative. | | rowHeight | number | ✓ | Pixel height of every row. Fixed; does not vary. | | renderRow | (node, data, ctx) => void | ✓ | Mutate node in place when it binds to data. | | poolSize | number | optional | Auto-computed from viewport + buffer. Override for predictable memory. | | buffer | { fwd, back, base } | optional | Default { 250, 30, 75 }. Smaller for memory-constrained scenarios. | | plugins | Plugin[] | optional | Plugin instances; see Plugins below. | | devMode | boolean | optional | Asserts invariants on every operation; throws on contract violation. | | onError | (err, ctx) => void | optional | Error handler for plugin/render errors. |

Instance methods (Stable since 1.0)

instance.setData(data)              // replace underlying data; preserves scroll
instance.invalidate(dataId)         // re-render a single row (O(1))
instance.scrollToIndex(idx, { align, behavior })
instance.destroy()                  // teardown; idempotent
instance.on(event, handler) → unsubscribe
instance.off(event, handler)
instance.isScrolling()              // boolean
instance.getRange() → { start, end }
instance.getPool()                  // beta — read-only pool view

Instance methods (Stable since 1.2)

instance.extend(namespace, methods)          // sanctioned plugin namespace channel
instance.scheduleAfter(ms, fn) → handle      // destroy-coordinated setTimeout

See API.md for full signatures, complexity, and stability guarantees.


Events

| Event | Payload | When fired | |---|---|---| | init | { instance } | After create() returns | | bind | { node, data, ctx } | A pool node was bound to new data | | unbind | { node } | A pool node was released | | scroll | { scrollTop, direction, velocity, isScrolling } | After scroll handler processes an event | | range | { start, end, prevStart, prevEnd } | Visible range changed | | resize | { width, height } | Container resized | | render | { count, durationMs } | Render cycle completed | | destroy | {} | Before instance teardown |


Plugins

Plugins are how V2+ features land without modifying core. A plugin is a function returning a hook object:

function MyPlugin(opts = {}) {
  return {
    name: 'my-plugin',
    onInit(instance)         { /* setup; call instance.extend('myns', {...}) here */ },
    onBind(node, data, ctx)  { /* row got new data */ },
    onUnbind(node)           { /* row released */ },
    onScroll(state)          { /* scrolled */ },
    onResize(state)          { /* viewport resized */ },
    onRender(range)          { /* visible range changed */ },
    onDestroy()              { /* teardown — timers via scheduleAfter auto-cancel */ },
  };
}

All hooks optional. Pass via plugins option.

Built-in plugins

| Plugin | Status | Purpose | |---|---|---| | flash | V1.2 shipped | Pool-aware row flash. Reference plugin shape. See plugins/flash/. | | scrollRestore | V1.3 planned | Persist + restore scroll position via localStorage. | | keyboardNav | V1.3 planned | Arrow-key navigation between rows. | | stickyGroups | V2.0 planned | Sticky group headers. | | animation | V2.0 planned | Row insert / remove / move animations. |

Plugin contract guarantees:

  1. Hooks called in registration order.
  2. Errors caught — one plugin's bug never breaks another or core.
  3. extend() is the only sanctioned channel for namespace mutation; scheduleAfter() is the only sanctioned channel for plugin timers (auto-cancelled in destroy()).
  4. Plugins observe — they don't intercept.

Full contract in ARCHITECTURE.md §4.2.


Performance & capacity

Tested operating envelope (Chrome 147 / macOS Intel, 5× median):

| Tier | Rows | Cold render (typ.) | p99 frame (typ.) | Sustained scroll | |---|---|---|---|---| | Sweet spot | 0 – 10,000 | 14–17 ms | 17.6 ms | 60 fps | | Designed-for | 10k – 100,000 | 14–16 ms | 17.6 ms | 60 fps | | Stretch | 100k – 500,000 | 14–25 ms | 17.6–67 ms | 49–60 fps |

Hot-path contracts (asserted by Node bench in CI):

  • Scroll handler ≤ 4 ms p99 at every tier
  • Per-flash-call ≤ 0.05 ms p50 (when flash plugin is loaded)
  • Heap delta ≤ 100 KB after 1000 scroll events

See BENCHMARKS.md for full head-to-head measurement against TanStack Virtual and virtua, including methodology and disclaimers.


Browser support

  • Chrome / Edge: 90+
  • Firefox: 88+
  • Safari: 14+ (modern WebKit)
  • Mobile Safari iOS: 14+
  • Mobile Chrome Android: 90+

Requires native ES module import, ResizeObserver, requestAnimationFrame, and CSS transform: translate3d. No polyfills shipped.


Documents in this folder

| File | Purpose | |---|---| | README.md (this) | Quickstart + API surface | | ARCHITECTURE.md | Design rationale, SOLID mapping, plugin contract | | API.md | Function-level registry (every public + internal fn) | | CONTRACTS.md | Invariants + capacity envelope | | CHANGELOG.md | Version history (Keep-a-Changelog format) | | BENCHMARKS.md | Performance baselines + head-to-head with peers | | PRIOR_ART.md | Library research + failure modes inherited from peers | | FUTURE_IDEAS.md | Backlog of deferred features with triggers | | DscrollFlash.md | V1.2 flash plugin spec (Round Table locked) | | LICENSE | MIT | | NOTICE.md | Third-party notices (none for runtime; peer benches only) |


Running tests + benchmarks

npm test                                # 301 tests across 5 files
npm run bench                           # Node-based flash plugin perf gates

# Browser-based benches (require local HTTP server, not file://)
python3 -m http.server 8080
# open http://localhost:8080/bench/peers.html        # 5-library × 5-tier head-to-head
# open http://localhost:8080/bench/run.html          # capacity sweep
# open http://localhost:8080/bench/v1-vs-v2.html     # row-renderer micro-bench
# open http://localhost:8080/stress.html             # interactive stress test

Contributing

PRs welcome. Every change must:

  1. Add a CHANGELOG.md entry.
  2. Update API.md if public surface changes.
  3. Pass npm test (all 301 tests).
  4. Not regress npm run bench (5 perf gates).

Public-API changes require a deprecation period. Internal API (_prefix) changes free.


License

MIT. Copyright (c) 2026 Dharmesh Patel. See LICENSE.