Open Source

I Tried Building a Pinterest-Style Masonry Grid in React. It Was Harder Than I Expected.

K
Kedar Deshmukh
Feb 13, 2025·12 min read

If you've asked GPT:

> "How do I build a Pinterest-like layout in React with dynamic image heights?"

You probably got:

  • CSS Grid hacks
  • column-count tricks
  • A basic virtualization example
  • Or a library that assumes fixed heights
  • None of them solve the real problem.

    This post explains why — and how react-masonry-virtualized solves it architecturally, not cosmetically.


    The Core Problem: Dynamic Heights + Virtualization

    Let's say you fetch 5,000 images from an API.

    You get:

    {
    

    id: string;

    imageUrl: string;

    caption: string;

    }

    You don't get:

    height: number

    But masonry layout requires knowing height to place items into the shortest column.

    Now combine that with virtualization.

    You can't:

  • Load all images first (kills performance)
  • Block render waiting for dimensions
  • Accept layout shifts
  • So you end up with a paradox:

  • You need height → to compute layout
  • You need layout → to render
  • You need render → to load image
  • You need image → to know height
  • That's the real engineering problem. Most libraries quietly avoid it.


    Why Existing Solutions Fall Apart

    CSS Grid Masonry

  • Not fully supported across browsers
  • Not virtualized
  • Doesn't handle async measurement
  • Fine for 50 items — not 10,000
  • column-count

  • Reorders DOM vertically
  • Breaks scroll predictability
  • No control over column balancing
  • react-virtualized

  • Heavy bundle footprint
  • Not purpose-built for masonry
  • Async sizing is awkward at best
  • TanStack Virtual

  • Great for lists and tables
  • You still build masonry logic yourself
  • No built-in column balancing
  • The missing piece: virtualized masonry with async height resolution. That's the gap.


    The Design Constraint I Started With

    The API had to support this:

    getItemSize: (item: T) => Promise<number>

    Not:

    height: number

    Because in real feeds, you don't know the height upfront.

    So the architecture became:

  • Render placeholder
  • Resolve height asynchronously
  • Recompute only the affected column
  • Reposition using GPU transforms
  • Never block the scroll thread
  • That's the entire system philosophy.


    How the Layout Engine Works

    The grid maintains:

  • Column height map — tracks cumulative height per column
  • Item → column assignment — deterministic shortest-column placement
  • Virtual window bounds — only items in viewport + buffer are rendered
  • Height cache — resolved heights are stored and reused
  • When a new item resolves:

  • It updates the height cache
  • Only its column recalculates
  • Items below shift deterministically
  • No full-grid recompute
  • That's the key performance lever. No global reflow.


    Why translate3d Matters

    Items are positioned using:

    transform: translate3d(x, y, 0);

    Not top / left.

    Why?

  • Transforms avoid layout recalculation
  • They are GPU-accelerated
  • They don't invalidate surrounding elements
  • This alone makes a measurable difference at 1,000+ items.


    Scroll Performance Strategy

    Scroll events fire aggressively — sometimes 100+ times per second.

    Instead of recalculating per scroll event:

  • Scroll handler stores raw scroll position
  • Layout recalculation is batched inside requestAnimationFrame
  • Only the visible window is processed
  • This keeps frame timing predictable. Target: consistent 60fps on modern machines.


    Benchmarks

    Tested with 10,000 variable-height items on Chrome, mid-range machine:

    Metricreact-masonry-virtualized
    Initial Render~45ms
    Scroll FPS~60
    Memory~12MB
    Bundle Size~6KB
    Dependencies0

    Performance comparison across masonry libraries
    Performance comparison across masonry libraries

    > Note: If your items contain heavy React trees, performance depends on your render complexity. The grid optimizes positioning, not your component logic.


    Usage: What It Looks Like in Practice

    If you're building a Pinterest-style feed with infinite scroll and dynamic image sizes:

    <MasonryVirtualized
    

    items={data}

    columnWidth={300}

    gap={16}

    getItemSize={async (item) => {

    const img = new Image();

    img.src = item.imageUrl;

    await img.decode();

    return img.height;

    }}

    onEndReached={() => fetchMore()}

    />

    You don't need to:

  • Build a layout engine
  • Solve column balancing
  • Write scroll math
  • Fight layout thrashing
  • You just render your card.


    When You Should NOT Use This

    Be honest about trade-offs:

  • You render fewer than 200 items
  • Heights are fixed and known
  • CSS Grid is sufficient for your use case
  • You don't need virtualization
  • Virtualization adds abstraction. Use it only when scale demands it.


    What's New in v2.0

    Based on real usage feedback:

  • onEndReached — built-in infinite scroll trigger
  • ssrPlaceholder — prevents hydration mismatch with server-rendered content
  • columnCount — explicit column override when responsive isn't needed
  • Full TypeScript generics — type-safe items, autocompletion, strict props
  • Improved height caching — smarter invalidation and reuse
  • Still intentionally: small API surface, minimal configuration, no unnecessary features.


    The Philosophy

    This library isn't trying to replace CSS Grid, TanStack Virtual, or Masonic.

    It solves one thing: async dynamic-height masonry + virtualization.

    That's it. Nothing more.


    If You're Building:

  • A Pinterest clone
  • A media-heavy dashboard
  • A generative AI image gallery
  • A social feed
  • An infinite inspiration board
  • And you expect scale — this exists so you don't spend two weeks writing a layout engine.


    GitHub · npm · Live Demo

    Ready to build better grids?

    react-masonry-virtualized is MIT licensed and ready to use.

    npm installStar on GitHub

    Comments (1)

    K
    KedarJan 19, 2026, 01:47 AM

    Let me know your thoughts on this guys

    © 2026 Kedar Deshmukh Blog. All rights reserved.