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

dom-attic

v0.3.0

Published

Mount heavy components once, show cheap snapshots, revive on interaction. Framework-agnostic DOM core.

Readme

dom-attic

Mount heavy components once, show cheap snapshots, revive them on interaction.

Why

List virtualization saves DOM at the cost of remounting: a row scrolls out of view and its components are destroyed, it scrolls back and they are built from scratch. When a cell is expensive (a select with 10,000 options, an editor, a chart), scrolling turns into endless rebuilding, and every cell loses its internal state along the way.

dom-attic inverts that. A component is mounted once for the lifetime of the page and is never recreated. When it is not needed, its DOM node moves into a detached container, where there is no layout and no paint, yet the component stays alive and keeps receiving updates. In its place the host keeps an inert snapshot, a cloneNode of the live node.

As soon as the user touches the snapshot, it steps aside and the real component takes over with all of its state intact.

Install

npm i dom-attic

Core (no framework)

The core is plain DOM. It does not import Vue, React or anything else.

import { Attic, attachInteraction } from 'dom-attic'

const attic = new Attic({ liveLimit: 20 })

attic.register('row-1:status', hostElement) // host already holds the mounted node
attic.park('row-1:status')                  // node goes to storage, a snapshot stays behind

const detach = attachInteraction(attic, tableRoot) // one listener for the whole list

| Method | What it does | |---|---| | register(key, host) | put a cell under management | | park(key) | move the live node away, leaving a fresh snapshot | | revive(key) | bring the live node back, evicting the oldest beyond liveLimit | | markDirty(key) / refresh(key) | data changed, so retake the snapshot | | stats | how many cells are registered, live and parked | | dispose() | return every node to the document for the framework to unmount |

Options

new Attic({
  liveLimit: 20,      // how many live nodes may sit in the document at once
  replayEvents: true, // repeat the original event on the revived node
  onEvict: (key) => {},
})

Vue

<script setup>
import { AtticCell, useAtticRoot } from 'dom-attic/vue'

const { rootRef, stats } = useAtticRoot({ liveLimit: 20 })
</script>

<template>
  <div ref="rootRef">
    <AtticCell
      v-for="row in rows"
      :key="row.id"
      :cell-key="row.id"
      :revision="row.updatedAt"
    >
      <HeavySelect v-model="row.status" :options="tenThousand" />
    </AtticCell>
  </div>
</template>

useAtticRoot creates the storage and wires interaction to rootRef. AtticCell registers its own root element, parks it right after mount, and retakes the snapshot whenever revision changes. Pass :park-on-mount="false" for cells that must stay live.

A React adapter is planned along the same lines: the core knows nothing about frameworks.

How the first click survives

A snapshot is inert, so the click has to reach the real component. On pointerdown the library records the path to the pressed element, swaps the snapshot for the live node, finds the twin element by that same path and replays the event on it. Without this the first click would be swallowed: the browser never delivers click to an element that left the document.

Surviving virtualization

A virtualized list removes rows from the DOM, and a framework destroys whatever it mounted inside them, snapshots included. The farm keeps that content mounted out of sight and lends a node to whichever placeholder shows it right now:

<script setup>
import { AtticFarm, AtticSlot, useFarm } from 'dom-attic/vue'

useFarm()
</script>

<template>
  <!-- Rendered once per key, wherever the rows are or are not -->
  <AtticFarm :keys="warmKeys" v-slot="{ cellKey }">
    <HeavySelect :model-value="valueOf(cellKey)" :options="tenThousand" />
  </AtticFarm>

  <!-- Inside the virtualized row: shows the node, mounts nothing -->
  <AtticSlot :cell-key="`${row.id}:status`" />
</template>

Scrolling a row away releases the node back to the farm; scrolling it back adopts the very same node, with its state intact.

keys may arrive in any order: the farm renders them sorted, so reordering the data (a sort, a drag) never moves a node a placeholder is currently showing. Keep the set bounded, though: it is exactly the set of cells you are willing to keep alive at once. The farm grows in idle slices of chunk entries (20 by default) so a few hundred heavy cells do not mount in one patch and freeze the tab.

Limitations

  • cell content must be a single root element, only the first node is snapshotted;
  • canvas and video are not carried over into a snapshot, mount such cells with parkOnMount: false;
  • tooltips and popovers that live outside the cell do not work on a snapshot until it revives;
  • a cell holding focus should not be evicted, so keep liveLimit comfortable.

Development

npm run dev        # playground
npm test           # unit tests, run in a real browser
npm run test:e2e   # interaction scenarios
npm run build

Tests run in Chromium rather than a DOM emulator: the library works with detached nodes, focus and synthetic clicks, and emulation is not faithful enough there.

License

MIT