A high-performance, virtualized masonry grid component for React with dynamic column layout, infinite scroll, and async image loading
npm install react-masonry-virtualizedA high-performance, virtualized masonry grid component for React with dynamic column layout and lazy loading.
- π High Performance: Virtual scrolling renders only visible items
- π± Responsive: Automatically adjusts columns based on container width
- π¨ Flexible: Works with any content type (images, cards, etc.)
- πͺ TypeScript: Full type safety and IntelliSense support
- β‘ Optimized: Uses RAF, memoization, and CSS containment
- π― Zero Dependencies: Only peer dependencies on React
- π¦ Lightweight: < 6KB minified
- βΎοΈ Infinite Scroll: Built-in onEndReached callback
- π₯οΈ SSR Ready: Placeholder support for hydration
``bash`
npm install react-masonry-virtualized
`bash`
yarn add react-masonry-virtualized
`bash`
pnpm add react-masonry-virtualized
`tsx
import { MasonryGrid, getImageSize } from 'react-masonry-virtualized';
const images = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
// ... more images
];
function App() {
return (
renderItem={(src, index) => (
src={src}
alt={Image ${index}}`
loading="lazy"
style={{ width: '100%', height: 'auto', display: 'block' }}
/>
)}
getItemSize={async (src) => await getImageSize(src)}
gap={16}
minWidth={280}
/>
);
}
`tsx
import { MasonryGrid, getImageSize } from 'react-masonry-virtualized';
function App() {
const [images, setImages] = useState(initialImages);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
if (loading) return;
setLoading(true);
const newImages = await fetchMoreImages();
setImages(prev => [...prev, ...newImages]);
setLoading(false);
};
return (
renderItem={(src, index) => (
Image ${index}
} loading="lazy" />`
)}
getItemSize={async (src) => await getImageSize(src)}
onEndReached={loadMore}
onEndReachedThreshold={500}
/>
);
}
If you already know item dimensions, return them immediately for better performance:
`tsx`
renderItem={(post) =>
getItemSize={(post) => Promise.resolve({
width: post.width,
height: post.height
})}
/>
`tsx`
renderItem={(src) => }
getItemSize={async (src) => await getImageSize(src)}
ssrPlaceholder={
{[...Array(9)].map((_, i) => (
))}
}
/>
`tsx`
columnCount={4} // Always 4 columns
// ... other props
/>
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | T[] | required | Array of items to render |renderItem
| | (item: T, index: number) => ReactNode | required | Function to render each item |getItemSize
| | (item: T, index: number) => Promise<{width, height}> | required | Function to get item dimensions |baseWidth
| | number | 241 | Base width for scaling calculations |minWidth
| | number | 223 | Minimum width for each column |gap
| | number | 16 | Gap between items in pixels |className
| | string | '' | Container class name |style
| | CSSProperties | undefined | Container inline styles |bufferMultiplier
| | number | 1 | Viewport buffer (1 = 1 viewport above/below) |columnCount
| | number | undefined | Override auto column count |onEndReached
| | () => void | undefined | Callback when scrolled near end |onEndReachedThreshold
| | number | 500 | Distance from end to trigger callback (px) |ssrPlaceholder
| | ReactNode | undefined | Placeholder during SSR/loading |disableVirtualization
| | boolean | false | Render all items (disables virtual scroll) |
#### getImageSize(src: string): Promise<{width, height}>
Helper function to load image dimensions. Useful for image-based masonry grids.
`tsx
import { getImageSize } from 'react-masonry-virtualized';
const dimensions = await getImageSize('https://example.com/image.jpg');
// { width: 1920, height: 1080 }
`
1. Dynamic Columns: Calculates optimal number of columns based on container width and minWidthReact.memo
2. Masonry Layout: Places items in the shortest column (Pinterest-style)
3. Virtual Scrolling: Only renders items visible in viewport + buffer
4. Performance Optimization:
- prevents unnecessary re-rendersuseCallback
- memoizes expensive calculationsrequestAnimationFrame
- throttles scroll eventstranslate3d
- Debounced resize handler
- CSS containment for layout isolation
- GPU-accelerated transforms with
1. Memoize getItemSize: If dimensions don't change, cache them
2. Use pre-computed dimensions: Return Promise.resolve() for known sizesbufferMultiplier
3. Adjust : Lower values render fewer items (faster) but may show blank space while scrollingloading="lazy"`: For images, enable native lazy loading
4. Use
5. Optimize images: Use appropriately sized images
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
MIT
Contributions welcome! Please open an issue or PR.
Built with β€οΈ using React, TypeScript, and tsup.