Pinterest Waterfall Grid Masonry in React Native
Two things fight each other: which column an item goes in, and rendering thousands of mixed-height images without loading them all.
TL;DR
A Pinterest-style masonry grid packs variable-height items into columns tightly, which a uniform grid cannot do, and it is deceptively hard because layout and performance fight: which column each item goes in (the shortest-column algorithm) versus rendering a feed of thousands of mixed-height images at 60fps. The build is a virtualized masonry: place each item in the currently-shortest column, reserve its height from the image aspect ratio in your data before the image loads (killing reflow jank), and virtualize with FlashList's masonry layout (over a million weekly downloads) so only the visible window renders. Stream images into reserved space with caching and capped decodes, and adapt column count per device. A free VP0 design supplies the waterfall feed screens.
What makes a masonry grid hard, and why not just use columns?
The variable heights. A Pinterest-style waterfall grid places items of different heights into columns so they pack tightly with no big gaps, which a plain fixed-grid cannot do because its cells are uniform. The visual is simple to picture and deceptively hard to build well, because two things fight each other: the layout (which column does each item go in, and how tall is it) and the performance (a feed of thousands of mixed-height images must scroll at 60fps without loading them all). Get either wrong and you have a janky or gappy grid, which is the opposite of the effortless flow the pattern promises.
The honest framing first: the naive masonry (render everything, measure, reflow) works for 20 items and dies at 2,000, so the real build is a virtualized masonry that only renders what is on screen, and the layout algorithm has to work without knowing every item’s height in advance. That constraint, computing the layout incrementally as items load, is what separates a real waterfall grid from a demo.
How does the column-packing actually work?
By always adding the next item to the shortest column, which keeps the columns balanced:
| Step | What happens | Why |
|---|---|---|
| Track column heights | Keep a running height per column | The basis for placement |
| Place next item | Add it to the currently-shortest column | Keeps the bottom edges even |
| Know item height | From aspect ratio (width is fixed per column) | Compute height before the image loads |
| Recompute on resize | Re-pack when column count changes | Rotation / iPad changes the layout |
The shortest-column rule is the whole algorithm: each new item goes wherever the bottom is highest up, so the columns grow evenly and the grid stays tight. The key enabler is knowing an item’s height before its image loads, which you get from the aspect ratio in the data (the API returns image width and height, so at a fixed column width you compute the rendered height and reserve the space), avoiding the layout-shift jank of items resizing as images arrive. Computing height from metadata, not from the loaded image, is the detail that makes the grid stable.
How do you keep it at 60fps?
By not rendering the whole feed. The performant path uses a virtualized list that supports masonry: FlashList (1,721,107 weekly downloads) ships a masonry layout that recycles cells and only renders the visible window, which is the difference between a grid that scrolls forever and one that crashes at a few hundred items. Building masonry on a plain ScrollView or a non-virtualized map over the data is the classic mistake, the same render-only-what-is-visible discipline as any long-list performance fix.
Image handling completes the performance story: lazy-load images as cells enter the viewport (a library like expo-image handles caching and placeholders), show a placeholder at the reserved aspect-ratio size (so there is no reflow), cache decoded images, and cap concurrent decodes so a fast scroll does not try to decode 50 images at once. The grid feels effortless only when the layout is stable and the images stream in without shifting anything, which is entirely a function of reserving space from aspect ratio and virtualizing the render.
What completes a waterfall feed?
The interaction and the polish. Tap-to-detail (often with a shared-element transition from the grid cell to the full view, the hero animation pattern), pull-to-refresh, and infinite scroll that loads the next page before the user hits the bottom. Two device honesties: the column count should adapt (two columns on a phone, more on an iPad or in landscape), recomputing the layout on size change, and the grid should handle the empty and loading states gracefully (skeleton cells at varied heights, not a blank screen).
The screens, the waterfall feed, the cell, the detail transition, come as a free VP0 design, so an agent builds the shortest-column layout and FlashList virtualization onto a UI already shaped for aspect-ratio-reserved cells and adaptive columns rather than a naive map that janks. The library-level view of this genre, where the whole VP0 catalog itself is a masonry browse, is the Pinterest-style app UI library.
Key takeaways: a masonry waterfall grid
- The difficulty is variable heights: items of different heights packed tightly, which a uniform grid cannot do.
- Shortest-column placement is the algorithm: add each item to the currently-shortest column to keep the grid tight.
- Reserve height from aspect ratio: compute rendered height from the data’s image dimensions before the image loads, to avoid reflow jank.
- Virtualize with FlashList masonry: render only the visible window; a non-virtualized map over thousands of items janks or crashes.
- Stream images into reserved space: lazy-load with aspect-ratio placeholders, cache, and cap concurrent decodes for a stable 60fps feed.
Frequently asked questions
How do I build a Pinterest-style masonry grid in React Native? Use a virtualized masonry list (FlashList’s masonry layout), place each item in the currently-shortest column, and reserve each cell’s height from the image aspect ratio in your data before the image loads to avoid reflow. Lazy-load images into the reserved space. A free VP0 design supplies the waterfall feed and cell screens.
Why is a masonry grid harder than a normal grid? Because the items have variable heights that must pack tightly with no gaps, which a uniform fixed grid cannot do, and because a feed of thousands of mixed-height images must scroll at 60fps without rendering them all. The layout (which column, what height) and the performance (virtualization) fight each other, and both must be solved.
How do I stop the grid from jumping as images load? Reserve each cell’s height before the image loads by computing it from the image’s aspect ratio in your data (the API’s width and height at a fixed column width), and show a placeholder at that size. Computing height from metadata rather than the loaded image is what removes the layout-shift jank.
How do I keep a masonry feed performant? Virtualize it: use FlashList’s masonry layout, which recycles cells and renders only the visible window, instead of a plain ScrollView or a non-virtualized map over the data that janks or crashes past a few hundred items. Pair it with lazy image loading, caching, and a cap on concurrent decodes.
How does the column-packing algorithm work? By always adding the next item to the column that is currently shortest, tracking a running height per column, so the columns grow evenly and the bottom edges stay close together. Knowing each item’s height ahead of time, from its aspect ratio, lets the algorithm place items without waiting for images to load.
Other questions from VP0 builders
How do I build a Pinterest-style masonry grid in React Native?
Use a virtualized masonry list (FlashList's masonry layout), place each item in the currently-shortest column, and reserve each cell's height from the image aspect ratio in your data before the image loads to avoid reflow. Lazy-load images into the reserved space. A free VP0 design supplies the waterfall feed and cell screens.
Why is a masonry grid harder than a normal grid?
Because the items have variable heights that must pack tightly with no gaps, which a uniform fixed grid cannot do, and because a feed of thousands of mixed-height images must scroll at 60fps without rendering them all. The layout and the performance fight each other, and both must be solved for the grid to feel effortless.
How do I stop the masonry grid from jumping as images load?
Reserve each cell's height before the image loads by computing it from the image's aspect ratio in your data (the API's width and height at a fixed column width), and show a placeholder at that size. Computing height from metadata rather than the loaded image is what removes the layout-shift jank.
How do I keep a masonry feed performant?
Virtualize it: use FlashList's masonry layout, which recycles cells and renders only the visible window, instead of a plain ScrollView or a non-virtualized map over the data that janks or crashes past a few hundred items. Pair it with lazy image loading, caching, and a cap on concurrent decodes.
How does the masonry column-packing algorithm work?
By always adding the next item to the column that is currently shortest, tracking a running height per column, so the columns grow evenly and the bottom edges stay close together. Knowing each item's height ahead of time, from its aspect ratio, lets the algorithm place items without waiting for images to load.
Part of the React Native & Expo: Mobile Frontend Architecture hub. Browse all VP0 topics →
Keep reading
Build an NS Flex Travel History Timeline in React Native
A travel history timeline lists past journeys by date. Here is how to build the NS Flex trip-history screen in React Native with fast scrolling and offline cache.
Build Infinite Scroll in React Native with TanStack Query
TanStack Query handles paging, a virtualized list handles rendering. Here is how to build infinite scroll in React Native with useInfiniteQuery and FlashList.
React Native Bundle Size Optimization for AI Apps
AI apps bloat because agents add and never remove. Optimization is subtraction: measure with a visualizer, cut the heaviest libraries, lazy-load, right-size assets.
React Native Game Loop Engine Hook
React's event model fights a per-frame tick: run a frame-synced loop off the JS thread with Reanimated, pass delta time, and add start/stop/pause.
React Native New Architecture: The Bridgeless UI Reality
A bridgeless UI kit is just current components that avoid legacy-bridge assumptions. The work is dependency-first, not a new component language.
3D Model Viewer Carousel in React Native: One Context
Build a 3D model viewer carousel in React Native: one GL context with swapped models, glTF with Draco, phone-grade poly budgets, and honest loading states.