Why Pools? LITE+
WildflowerJS gives you two reactivity modes for collections. data-list is push reactivity: mutate state, the DOM updates automatically. data-pool is pull reactivity: mutate plain objects, tell the pool what changed, it updates on the next frame. Both are reactive. Same template syntax. Different tradeoff.
The Insight
Push reactivity is convenient: mutate a property, the DOM updates. But that convenience has a cost: every item gets wrapped in a reactive proxy, every property write goes through a trap, and every change triggers dependency tracking.
For a 20-item settings panel, that cost is invisible. For a 10,000-row table or a real-time dashboard, it adds up.
WildflowerJS lets you choose. data-pool is still reactive (state changes still reach the DOM) but through a pull model instead of push. You trade automatic change detection for explicit markDirty() calls, and get plain-object mutation speed in return.
Two Reactivity Modes
data-list (Push) |
data-pool (Pull) |
|
|---|---|---|
| Item type | Reactive proxy objects | Plain JS objects |
| Update trigger | Property change via Proxy | requestAnimationFrame batch |
| Update cost | Per-property (precise, zero idle cost) | Per-frame (batched, throughput-optimized) |
| Template scope | Full: item properties, parent state, stores, computed | Item properties + shared props (via pool.props) |
| Best for | Interactive items: forms, inline editing, two-way binding | Large datasets, real-time data, animation, performance-sensitive CRUD |
| Template syntax | Identical: data-bind, data-bind-style, data-bind-class, data-bind-attr, data-show |
|
The developer picks the right mode for the job. No escape hatches. No external libraries. Swap data-list for data-pool and you switch from push to pull. The template stays the same.
Use Cases
Large Collections
Log tables, audit trails, activity feeds, search results, product catalogs, leaderboards: any collection where explicit update control beats automatic change detection. Pool renders with zero proxy overhead, handles full CRUD via its API, and offers optional entity recycling for pagination.
Real-Time Data
Stock tickers, monitoring dashboards, IoT sensor readings, WebSocket feeds: data that arrives pre-formed and just needs to be displayed. Mutate the plain object, the pool flushes it to DOM on the next frame. No reactive notifications, no microtask scheduling. With data-pool-fps throttling, each pool can update at its own frequency.
Per-Frame Animation
Particle systems, game entities, physics simulations, data visualizations: hundreds of DOM elements updating every frame at native refresh rate. This is where pools originally started, and where no other framework can follow. Every DOM element is a potential animation target. No canvas. No WebGL. No escape hatches.
Performance-Sensitive Lists
Pools aren't just for display-only data. They handle full CRUD operations (create, select, update, remove, swap) with the same template syntax as data-list. The tradeoff: you call markDirty() explicitly instead of relying on automatic change detection. In return, every mutation skips the proxy trap pipeline entirely.
Here's the same interactive list implemented both ways. The template is nearly identical: two attribute changes and a props. prefix:
data-list
<tbody data-list="rows" data-key="id">
<template>
<tr data-bind-class="id === selectedId ? 'danger' : ''">
<td data-bind="label"></td>
<td><a data-action="select">Select</a></td>
<td><a data-action="remove">Remove</a></td>
</tr>
</template>
</tbody>
data-pool
<tbody data-pool="rows" data-key="id" data-pool-static>
<template>
<tr data-bind-class="id === props.selectedId ? 'danger' : ''">
<td data-bind="label"></td>
<td><a data-action="select">Select</a></td>
<td><a data-action="remove">Remove</a></td>
</tr>
</template>
</tbody>
The JavaScript differs in how you express mutations:
data-list: automatic
// Selection: set state, framework handles it
this.state.selectedId = item.id;
// Update: mutate through proxy
this.state.rows[i].label += ' !!!';
// Remove: splice the array
this.state.rows.splice(index, 1);
data-pool: explicit
// Selection: set prop, mark only 2 rows dirty
pool.props.selectedId = item.id;
pool.markDirty(prevId);
pool.markDirty(item.id);
// Update: mutate plain object, mark dirty
item.label += ' !!!';
pool.markDirty(item.id);
// Remove: one call
pool.remove(item.id);
Where this matters most:
- Selection at scale: In a pool, you mark exactly 2 rows dirty: the old and new selection. Lists have a specialized refresh effect that also achieves O(2) for class-only deps, but pools reach it with zero framework machinery.
- Mutation overhead: Each list property write goes through a proxy trap: path string construction, dependency registration, effect scheduling. Pool items are plain objects. Mutate directly,
markDirty()once. - Bulk operations: Pool
clear()andadd()skip the per-item effect disposal and creation pipeline that lists require.
Pool vs List: Benchmark Comparison
Measured using the krausest js-framework-benchmark harness with identical templates:
| Operation | Pool vs List | Notes |
|---|---|---|
| Create 1,000 | -13% | No per-item effect creation |
| Update every 10th | -26% | Plain object mutation, no proxy traps |
| Select row | -19% | O(2) markDirty, zero framework overhead |
| Remove row | -22% | No effect disposal pipeline |
| Clear all | -27% | Bulk DOM removal, no per-item cleanup |
| Replace 1,000 | +1% | Similar; both use bulk path |
| Swap rows | +17% | List has O(1) swap fast path; pool re-renders both |
| Append 1,000 | +4% | Similar; both use bulk creation |
Pools win on targeted operations (update, select, remove, clear) where skipping the reactive pipeline matters most. Lists win on swap thanks to a specialized O(1) DOM swap path. Bulk creation operations are roughly equivalent.
The Architecture
Pools bypass the Proxy-based reactivity system entirely. Entity objects are plain JavaScript: no getters, no setters, no observable wrappers. Your code mutates properties directly, and the pool renderer reads those values and writes them to the DOM in a single batch.
// Display table: push plain objects, pool stamps them into DOM
data.forEach(row => this.pools.rows.push(row));
// Real-time update: mutate the object, pool picks it up next frame
this.pools.rows.update('row-42', { status: 'active' });
// Animation: mutate in a tick loop, pool flushes every frame
tick(dt) {
var t = dt / 16.67;
for (const p of this.pools.particles) {
p.x += p.vx * t;
p.y += p.vy * t;
}
}
This is why it's fast. There's no notification system, no dependency tracking, no change detection between your code and the DOM. The pool reads, diffs the previous values, and writes only what changed. The cost is proportional to the number of changed properties, not the number of entities.
Pools aren't a separate concept from the rest of the framework. They're entities, in exactly the sense that components, stores, and plugins are entities. The per-item declaration inside a pool's entity: block uses the same state, computed, and methods shape that components, stores, and plugins use at the top level. What makes pools different is multiplicity (one entity block describes many items, keyed by id), reactivity mode (items are plain JavaScript objects, pulled on flush, rather than Proxy-tracked state pushed on mutation), and timing (batched per-frame DOM writes rather than synchronous ones). See Defining an Entity for the full surface.
How Other Frameworks Handle This
They don't offer a choice. Every list item gets the full reactive treatment, whether it needs it or not.
| Framework | Large Collections | 60fps Animation |
|---|---|---|
| React | Full component per item, React.memo to reduce re-renders |
"Don't use setState in useFrame." Use refs + requestAnimationFrame to bypass React entirely. |
| Vue | Full reactive proxy per item via v-for |
toRaw() and shallowRef() as "escape hatches." External animation libraries. |
| Solid | Fine-grained signals per item via <For> |
Community recommends canvas-based rendering (tsParticles), not DOM. |
| Svelte | Full reactive binding per item via {#each} |
CSS animations recommended. No per-frame DOM rendering primitive. |
| WildflowerJS | data-pool: zero proxy overhead, plain objects |
data-pool + tick(): same primitive, native frame rate |
No other framework offers two rendering modes sharing the same template syntax. You don't escape the framework; you tell it how much reactivity you need.
When to Use Each
Use data-pool when:
- You have large collections (100+ items) where proxy overhead matters
- You need high-frequency updates (real-time data, animation, live feeds)
- You want explicit control over what gets re-rendered (
markDirty()) - You're paginating through data (entity recycling reuses DOM nodes)
- Items need shared state accessible via
pool.props(selection, filters, mode flags)
Use data-list when:
- Items need two-way binding (
data-model) - Items need to reference parent computed properties or store data in expressions
- Items need conditional rendering (
data-render) or nested lists - You want zero-ceremony reactivity: mutate state, DOM updates automatically
- The collection is small and interactive (forms, settings panels, editable tables)
data-list; it's the right default. Reach for data-pool when you need more performance or explicit control over what updates.