Design an Image Gallery
Concepts: Virtualization, Lazy Loading, Responsive Images (srcset), Masonry Layout, Intersection Observer, Placeholder/Blur-Up, Image CDN, Lightbox Pattern, CSS Grid/Flexbox
Requirements
Functional Requirements
| Requirement | Description |
|---|---|
| Grid display | Render images in a responsive grid layout |
| Infinite scroll | Load additional images as user scrolls |
| Lightbox | Display full-size image on click |
| Variable aspect ratios | Support images with different dimensions |
| Search and filter | Filter images by criteria |
| Responsive layout | Adapt to different screen sizes |
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Initial load | < 2 seconds | User retention |
| Scroll performance | 60 FPS | Smooth experience |
| Memory usage | < 200MB | Mobile device constraints |
| Accessibility | WCAG 2.1 AA | Inclusive design |
Technical Challenges
Rendering Large Image Sets
Rendering thousands of images as DOM nodes causes performance degradation.
Solution: Virtualization
Render only images within the visible viewport plus a buffer zone.
Libraries: react-window, react-virtualized, or custom Intersection Observer implementation.
Variable Height Images
Masonry layout (Pinterest-style) requires handling images with different aspect ratios.
| Approach | Advantages | Disadvantages |
|---|---|---|
| CSS Grid | Native browser support, simple | Limited masonry support |
| CSS Columns | True masonry layout | Column order, not row order |
| JavaScript calculation | Full layout control | Increased complexity, potential layout shifts |
| Fixed aspect ratio | Predictable layout | Image cropping required |
For Pinterest-style masonry, JavaScript position calculation provides the most control. For uniform grids, CSS Grid offers simpler implementation.
Progressive Image Loading
Loading full-resolution images immediately causes slow load times and high bandwidth usage.
Progressive loading strategy:
| Stage | Description |
|---|---|
| 1. Placeholder | Display blur or solid color |
| 2. Thumbnail | Load small preview (~20KB) |
| 3. Medium resolution | Load when image enters viewport |
| 4. Full resolution | Load on click or zoom |
Architecture
+-------------------------------------------------------------+
| React Application |
+-------------------------------------------------------------+
| +-------------+ +-------------+ +-----------------------+ |
| | Gallery | | Lightbox | | Search/Filter | |
| | Container | | Modal | | Controls | |
| +------+------+ +-------------+ +-----------------------+ |
| | |
| +------v----------------------------------------------+ |
| | Virtual Grid Component | |
| | +-----+ +-----+ +-----+ +-----+ +-----+ | |
| | | Img | | Img | | Img | | Img | | Img | ... | |
| | +-----+ +-----+ +-----+ +-----+ +-----+ | |
| +------------------------------------------------------+ |
+-------------------------------------------------------------+
| State Management (Context/Redux) |
+-------------------------------------------------------------+
| API Layer |
| GET /images?page=X&limit=Y&filter=Z |
+-------------------------------------------------------------+
Core Components
Virtual Grid
Implementation approach: Track scroll position in state and calculate visible image indices based on scroll offset, row height, and viewport height. Compute start and end indices for the visible range, converting row numbers to item indices by multiplying by the column count. Slice the images array to get only visible items. Render a container with the total calculated height to maintain proper scrollbar behavior. Position each visible image absolutely using calculated top positions. Listen for scroll events to update the scroll position and trigger re-render of visible items.
Lazy Loading with Intersection Observer
Implementation approach: Maintain two boolean states: one for whether the image has entered the viewport (inView), and one for whether the image has finished loading (loaded). Create an IntersectionObserver with a root margin to trigger loading before the image enters the visible area. When the element intersects, set inView to true and disconnect the observer. Render a placeholder div until the image loads. Only render the actual img element once inView becomes true. Listen for the onLoad event to set loaded to true and transition opacity from 0 to 1 for a smooth fade-in effect. Clean up the observer on component unmount.
Infinite Scroll
Implementation approach: Create a custom hook that maintains refs for both the IntersectionObserver and a sentinel element. When hasMore is true, create an observer with a generous root margin (such as 500px) to trigger loading before the user reaches the bottom. Observe the sentinel element and call loadMore when it intersects. Return the sentinel ref for the component to attach to an element at the end of the list. Clean up the observer on unmount or when dependencies change.
Image Optimization
Responsive Images
Serve appropriate image sizes based on device and container dimensions.
Implementation approach: Use the srcset attribute to provide multiple image sources at different widths (such as 300w, 600w, 1200w). Use the sizes attribute to specify the image display size at different viewport widths. The browser automatically selects the most appropriate image based on device pixel ratio and display size. Include a default src for browsers that do not support srcset.
Modern Formats
| Format | Size Reduction vs JPEG | Browser Support |
|---|---|---|
| WebP | 25-35% | 95%+ |
| AVIF | 50% | ~80% |
Use the picture element for format fallbacks. Include source elements for modern formats (AVIF, WebP) with type attributes. The browser uses the first supported format. Include a fallback img element with the JPEG source for maximum compatibility.
Blur-Up Placeholder
| Step | Description |
|---|---|
| 1 | Generate tiny (20x20) blurred version |
| 2 | Inline as base64 data URL |
| 3 | Display while full image loads |
| 4 | Crossfade to full image on load |
State Management
Data Structure
State organization: Use a normalized structure with images stored in a byId object for O(1) lookups, an allIds array for maintaining order, and a filteredIds array for current filtered results. Separate UI state (loading, error, hasMore, currentPage, selectedImageId, lightboxOpen) from filter state (search, dateRange, tags). This separation enables independent updates and reduces unnecessary re-renders.
API Integration
Implementation approach: Create a fetch function that accepts page, limit, and filter parameters. Build a query string using URLSearchParams. Make the API request and parse the JSON response. Return the images array, a hasMore boolean (calculated by comparing current page to total pages), and the total count. Handle errors appropriately and provide loading states to the UI.
Performance Optimizations
Debounce Scroll Events
Wrap the scroll handler in a debounce function with a 16ms delay (matching 60fps frame rate) to limit state updates during rapid scrolling. Memoize the debounced function to maintain a stable reference across renders.
Memoize Layout Calculations
Use useMemo to cache layout calculations (such as masonry positioning) and only recalculate when images or container width change. This prevents expensive layout computations on every render.
CSS Transform for Positioning
Use CSS transforms (translate3d) instead of top/left positioning. Transforms are GPU-accelerated and do not trigger layout recalculation, resulting in smoother animations and scrolling.
Content-Visibility
Apply the CSS content-visibility property with a value of auto to off-screen elements. The browser skips rendering these elements until they approach the viewport. Set contain-intrinsic-size to provide dimensions for layout calculation before rendering.
Accessibility
| Feature | Implementation |
|---|---|
| Keyboard navigation | Arrow keys move focus, Enter opens lightbox |
| Screen readers | Alt text on images, ARIA labels on controls |
| Reduced motion | Respect prefers-reduced-motion media query |
| Focus management | Trap focus in lightbox modal |
Design Trade-offs
| Decision | Options | Recommendation |
|---|---|---|
| Rendering | Full DOM vs Virtual | Virtual list for 100+ images |
| Layout | CSS Grid vs Masonry JS | CSS Grid for uniform; JS for variable heights |
| Image loading | Eager vs Lazy | Lazy with Intersection Observer |
| Placeholders | None vs Color vs Blur | Blur-up for visual continuity |
| State | Local vs Context vs Redux | Context for simple; Redux for complex filtering |