Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

blue-grid

DOM-recycling scroll engine for the web.
UITableView / RecyclerView-level performance with zero dependencies.
Single-column lists or multi-column grids — same recycling engine, same API.


The Problem

The browser was never designed for long lists. Render 10,000 items and you get 10,000 DOM nodes — sluggish scrolling, enormous memory footprints, and wasted layout passes on elements no one can see. The usual "virtual scroll" libraries help, but most are framework-specific, struggle with variable-height content, and break down once images load and heights shift.

blue-grid solves this the same way native mobile platforms have for over a decade: by recycling a small, fixed pool of real DOM nodes and repositioning them as the user scrolls. The total number of DOM nodes in play stays constant — whether your list has 100 items or 100,000.

What you get

  • Constant DOM count — only items in and around the viewport exist in the DOM.
  • 60 fps scrollingrequestAnimationFrame-gated scroll loop; no layout thrashing.
  • Variable-height items — heights are measured after render and cached. No need to know them up front.
  • Async-safeResizeObserver on every attached node catches image loads, font rendering, and dynamic content changes, then repositions automatically.
  • Scroll anchoring — position is tracked as { index, offset }, not raw scrollTop. When heights change, the viewport stays anchored to the same content.
  • Finite and infinite lists — pass a count for bounded data, or use prefetch for unbounded feeds with placeholder loading states.
  • Grid layout — pass item_width to switch to multi-column mode. Columns are derived from the container width and reflow on resize. By default, spacing uses space-evenly distribution. Add gap_min to fix the gap and let items stretch beyond item_width to fill available space. Same recycling, same anchoring, same performance.
  • Zero dependencies — vanilla JS, no framework, no virtual DOM, no build step required.

Install

bun add git+https://github.com/bluegate-studio/blue-grid.git

Then import it as an ES module:

import { BlueGrid } from 'blue-grid';

Or reference the source directly from a <script type="module"> tag — no bundler needed.


Usage

Finite List

When you have all your data up front:

import { BlueGrid } from 'blue-grid';

const items = [ /* your data array */ ];

const grid = new BlueGrid(document.querySelector('#list'), {
    count: items.length,

    cell(index, reuse) {
        const el = reuse || document.createElement('div');
        el.textContent = items[index].text;
        return el;
    },
});

cell(index, reuse) is called whenever the grid needs a DOM node for a given index. If reuse is not null, it's a previously used node — reconfigure it instead of creating a new one. This is where the recycling happens.

Infinite List (Async Loading)

When data is fetched on demand:

const data = [];

const grid = new BlueGrid(document.querySelector('#feed'), {

    cell(index, reuse) {
        const el = reuse || create_item_element();
        populate(el, data[index]);
        return el;
    },

    placeholder() {
        // Return a DOM node to show while data loads.
        // Optional — a shimmer bar is used by default.
        const el = document.createElement('div');
        el.className = 'skeleton';
        return el;
    },

    prefetch: async (range) => {
        const batch = await api.load(range.start, range.end);
        data.push(...batch);
        grid.set_count(data.length);
    },
});

When the user scrolls past loaded data, prefetch is called with the range of indices needed. Placeholder nodes fill the gap until you call set_count() with the new total, at which point real content replaces them.

Grid (Multi-Column)

When you want a card grid instead of a single-column list:

const grid = new BlueGrid(document.querySelector('#gallery'), {
    count: items.length,
    item_width: 240,
    gap_min: 12,

    cell(index, reuse) {
        const el = reuse || create_card_element();
        populate(el, items[index]);
        return el;
    },
});

item_width sets the minimum cell width. The number of columns is calculated from the container width and reflows on resize. gap_min fixes the gap at the specified value — items stretch beyond item_width to absorb excess space, preventing oversized margins on narrow viewports. Without gap_min, spacing uses space-evenly distribution and items stay at exactly item_width. Everything else — recycling, scroll anchoring, placeholders, prefetch — works identically.


API

new BlueGrid(container, config)

Parameter Type Required Description
container HTMLElement The scrollable container element
config.cell (index, reuse) => HTMLElement Return a DOM node for item at index. reuse is a recycled node or null
config.count number Total item count (default 0)
config.placeholder () => HTMLElement Factory for placeholder nodes (default: shimmer bar)
config.prefetch (range) => Promise<void> Called when items beyond loaded data approach the viewport
config.item_width number Minimum cell width in pixels. Enables multi-column grid mode
config.gap_min number Fixed gap in pixels between items and edges. Items stretch beyond item_width to fill space. Requires item_width

Instance Methods

Method Description
set_count(n) Update the total item count — triggers a re-render
reload() Re-render all visible items (like reloadData())
scroll_to(index) Scroll to a specific item by index
destroy() Remove all listeners, detach nodes, clean up

Acknowledgements

blue-grid is heavily inspired by the ideas and engineering behind:

  • Infinite Scroller — the Chrome team's reference implementation of DOM recycling and scroll anchoring for the web (2015). The techniques described in that post — tombstone placeholders, viewport-only rendering, anchor-based scroll tracking — are the conceptual foundation of this library.
  • UITableView — Apple's table view component, which pioneered cell reuse with dequeueReusableCell over a decade before the web caught up. The cell(index, reuse) API is a direct nod to this pattern.
  • RecyclerView — Android's adapter-based view recycler, which proved that a fixed pool of views plus a bind-by-index pattern scales to any dataset size.

With sincere thanks to the authors and developers of all three. Standing on the shoulders of giants.


License

MIT © Bluegate Studio

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages