Yeah, I get it. The world probably doesn't need another masonry library, right? I thought so too-until I spent two weeks wrestling with the ones out there in production.
Here's the deal.
It started with a photo grid
So, I was trying to create a Pinterest-style layout. Thousands of images, varying heights, and infinite scroll. Pretty standard fare if you've done any work on a content-heavy app.
First, I grabbed react-masonry-css. Honestly, it's a solid library-tiny bundle, CSS-based columns, and a super simple API. Everything was smooth until my dataset hit about 2,000 items, and then the browser started to slow down. Makes sense: it renders every single item to the DOM. No virtualization whatsoever. That's fine for a small gallery, but I was dealing with feeds that could swell to over 10K items.
Next, I turned to Masonic. This one actually virtualizes - it only puts visible items in the DOM, which is a big upgrade. But then I encountered a few annoying issues:
First off, the infinite scroll. Masonic doesn't handle that for you. You have to set up your own IntersectionObserver or scroll listener, deal with thresholds, manage loading states... not rocket science, but it's boilerplate I was just copying from project to project.
Then there were custom scroll containers. My grid was inside a scrollable panel, not the window. Masonic doesn't support that. I tried to make it work but threw in the towel after a day.
And let's not forget-it hasn't been updated since 2022. Not a total dealbreaker, but when you're hitting edge cases and the issues tab is crickets, it's a bit disheartening.
I spent another two weeks patching things together before I thought, "I'm just going to end up maintaining my own fork anyway. Might as well start fresh."
So I built DreamMasonry
The goal was to tackle my real issues without going overboard. Here's what I came up with.
Virtualization is always on. No need to configure it or even think about it. Throw 10,000 items at it, and there'll be about 40-50 DOM nodes in the container. The visible range gets recalculated on scroll using requestAnimationFrame, but React only re-renders when that range changes. So you get smooth 60fps scrolling even with crazy item counts.
<DreamMasonry
items={tenThousandPhotos}
maxColumnCount={4}
renderItem={(photo) => <img src={photo.src} />}
/>
Infinite scroll is built in. I got tired of doing this over and over. Now, you just need to pass the props:
<DreamMasonry
items={items}
renderItem={(item) => <Card item={item} />}
hasMore={hasMore}
isFetchingMore={loading}
onLoadMore={fetchNextPage}
scrollThreshold={2000}
renderLoader={() => <Spinner />}
/>
scrollThreshold defines how many pixels from the bottom the callback triggers. Loading and empty states? Those are just props. That's all there is to it.
Custom scroll containers are a go. This was the one that really pushed me to create my own solution. Sometimes, your masonry grid isn't sitting at the page level - maybe it's tucked inside a dashboard panel, a modal, or even a split view. Just pass a ref, and voilà - it works like a charm:
const scrollRef = useRef<HTMLDivElement>(null);
<div ref={scrollRef} style={{ height: '100vh', overflow: 'auto' }}>
<DreamMasonry
items={items}
renderItem={(item) => <Card item={item} />}
scrollContainer={scrollRef}
/>
</div>
The nerdy stuff
The layout engine uses Float64Array to keep track of column heights. Items are placed in the shortest column each time - classic masonry style - but using typed arrays means you get contiguous memory without number boxing. You won't see a difference at 100 items, but once you hit 10K, it's pretty noticeable.
There are zero dependencies other than React. The bundle size? About 13KB. For comparison, react-masonry-css is around ~3KB (though it lacks virtualization) and Masonic comes in at ~15KB, plus whatever else it pulls in.
Oh, and I exposed the internals as hooks if you want more control:
useGrid gives you virtualization with your own markup. usePositioner? That one's just the layout math - totally DOM-free, which is great for SSR or canvas rendering. And useInfiniteScroll is standalone; you can use it with anything.
Numbers
I put an interactive benchmark page on the demo site. Check it out for yourself, but here's what I found on my M1 MacBook:
| Items | Render Time | DOM Nodes | FPS |
|---|---|---|---|
| 100 | ~8ms | ~45 | 60 |
| 1,000 | ~25ms | ~45 | 60 |
| 5,000 | ~45ms | ~50 | 59 |
| 10,000 | ~65ms | ~50 | 58 |
Check out the DOM Nodes column - it barely budges. That's the crux of virtualization. Your browser isn't concerned if you have 100 or 100,000 items in that array. What matters is what's in the DOM.
When you shouldn't use this
If your grid's small and static - think under 200 items, no infinite scroll - then just go with react-masonry-css. It's easier and more lightweight. Seriously, you don't need virtualization for that.
And hey, if you're not using React, this won't be useful. It's strictly for React.
Also, if you need the entire grid rendered on the server (like all 10K items in the initial HTML), virtualization is really a client-side thing. You could utilize usePositioner to calculate layouts on the server side, but you'd have to figure out which subset to render.
Try it
npm install dream-masonry
Check out the demo to see it in action. You can look at the benchmarks to confirm the claims made.
If you decide to use it, a star on GitHub really helps with visibility. And if you find any issues or something's missing, just open an issue - I'm keeping this updated.
I'm Aditya. I build things at aucto.ai. This library was born out of real production struggles, not just a quick weekend project. Feel free to reach out on GitHub if you want to discuss it.
What masonry library are you using in production? What are the biggest pain points? I'm genuinely curious — drop a comment. Even if it's just "CSS columns work fine for me" — that's valid.
Top comments (0)