Demos
Same Code, Two Renderers
The same ProfileCard component rendered with react-dom (DOM) and react-pxl (Canvas). No code changes between the two — just swap the render call.
The component is standard JSX — <div>, <img>, <span>, <button>:
function ProfileCard() {
return (
<div style={{ width: 380, height: 460, backgroundColor: '#ffffff', borderRadius: 16 }}>
<div style={{ height: 120, backgroundColor: '#667eea' }} />
<div style={{ alignItems: 'center', marginTop: -40, padding: 20 }}>
<img src="avatar.svg" style={{ width: 80, height: 80, borderRadius: 40 }} />
<span style={{ fontSize: 20, fontWeight: 'bold' }}>Jane Cooper</span>
<span style={{ fontSize: 14, color: '#64748b' }}>Full-Stack Developer</span>
</div>
<button style={{ backgroundColor: '#4f46e5', color: '#fff', borderRadius: 10 }}>
Follow
</button>
</div>
)
}
// Canvas — swap one line:
import { render } from 'react-pxl'
render(<ProfileCard />, canvas)
// DOM — the original:
import { createRoot } from 'react-dom/client'
createRoot(el).render(<ProfileCard />)Scrollable List — 200 Items
A plain React list with 200 items inside overflow: 'scroll'. No virtualization, no special components — just .map() and standard elements.
function ActivityFeed() {
const items = Array.from({ length: 200 }, (_, i) => ({
id: i, name: names[i % 8], message: messages[i % 8],
}))
return (
<div style={{ width: 420, height: 520 }}>
<div style={{ fontSize: 18, fontWeight: 'bold' }}>Activity Feed</div>
<div style={{ flex: 1, overflow: 'scroll', gap: 6 }}>
{items.map(item => (
<div key={item.id} style={{ padding: 12, borderRadius: 10 }}>
<span style={{ fontWeight: 'bold' }}>{item.name}</span>
<span style={{ color: '#64748b' }}>{item.message}</span>
</div>
))}
</div>
</div>
)
}Benchmark: react-pxl vs react-window — 1000 Items, Dynamic Heights
Both lists render the same 1000 items with heights randomly varying between 40–200px (same seed for deterministic comparison). Scroll both and compare the live FPS counter in the top-right corner.
The Syntax Difference
react-pxl — standard JSX, no special API:
// Just a normal list. overflow: 'scroll' enables implicit virtualization.
function InfiniteList({ items }) {
return (
<div style={{ height: 520, overflow: 'scroll' }}>
{items.map(item => (
<div key={item.id} style={{ height: item.height, padding: 12 }}>
<span>{item.name}</span>
<span>{item.message}</span>
</div>
))}
</div>
)
}react-window — requires VariableSizeList, itemSize callback, render-prop:
import { VariableSizeList } from 'react-window'
// Must provide itemCount, itemSize function, and render callback.
// Can't use children directly — forced into index-based access.
function InfiniteList({ items, heights }) {
return (
<VariableSizeList
height={520}
width={420}
itemCount={items.length}
itemSize={(index) => heights[index]}
>
{({ index, style }) => (
<div style={{ ...style, padding: 12 }}>
<span>{items[index].name}</span>
<span>{items[index].message}</span>
</div>
)}
</VariableSizeList>
)
}| react-pxl | react-window | |
|---|---|---|
| Syntax | Standard JSX + .map() | VariableSizeList + render callback |
| Item heights | Automatic (Yoga layout) | Must provide itemSize function |
| Migration cost | Zero — swap render call | Rewrite list rendering code |
| Dynamic heights | Just works | Pre-measure or estimate + correct |
Animated Scroll: Height Magnification
Both lists apply scroll-position-driven height magnification to all 1000 items. Items near the viewport center expand (2×); items at the edges compress (0.6×). Every scroll frame re-computes heights, triggering a full layout pass.
Why canvas wins here: Changing every item’s height on each scroll frame is the worst case for DOM — react-window must call resetAfterIndex(0) to invalidate its size cache, then the browser re-lays out and repaints all repositioned elements. In react-pxl, it’s just a style number change → Yoga WASM relayout → single canvas redraw. No DOM mutations, no reflow.