DEV Community

Aditya Agarwal
Aditya Agarwal

Posted on

I Built a React Masonry Library That Handles 10K Items at 60fps

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} />}
/>
Enter fullscreen mode Exit fullscreen mode

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 />}
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check out the demo to see it in action. You can look at the benchmarks to confirm the claims made.

GitHub · npm

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)