Skip to main content

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

RequirementDescription
Grid displayRender images in a responsive grid layout
Infinite scrollLoad additional images as user scrolls
LightboxDisplay full-size image on click
Variable aspect ratiosSupport images with different dimensions
Search and filterFilter images by criteria
Responsive layoutAdapt to different screen sizes

Non-Functional Requirements

RequirementTargetRationale
Initial load< 2 secondsUser retention
Scroll performance60 FPSSmooth experience
Memory usage< 200MBMobile device constraints
AccessibilityWCAG 2.1 AAInclusive 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.

Loading diagram...

Libraries: react-window, react-virtualized, or custom Intersection Observer implementation.

Variable Height Images

Masonry layout (Pinterest-style) requires handling images with different aspect ratios.

ApproachAdvantagesDisadvantages
CSS GridNative browser support, simpleLimited masonry support
CSS ColumnsTrue masonry layoutColumn order, not row order
JavaScript calculationFull layout controlIncreased complexity, potential layout shifts
Fixed aspect ratioPredictable layoutImage 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:

StageDescription
1. PlaceholderDisplay blur or solid color
2. ThumbnailLoad small preview (~20KB)
3. Medium resolutionLoad when image enters viewport
4. Full resolutionLoad 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

FormatSize Reduction vs JPEGBrowser Support
WebP25-35%95%+
AVIF50%~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

StepDescription
1Generate tiny (20x20) blurred version
2Inline as base64 data URL
3Display while full image loads
4Crossfade 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

FeatureImplementation
Keyboard navigationArrow keys move focus, Enter opens lightbox
Screen readersAlt text on images, ARIA labels on controls
Reduced motionRespect prefers-reduced-motion media query
Focus managementTrap focus in lightbox modal

Design Trade-offs

DecisionOptionsRecommendation
RenderingFull DOM vs VirtualVirtual list for 100+ images
LayoutCSS Grid vs Masonry JSCSS Grid for uniform; JS for variable heights
Image loadingEager vs LazyLazy with Intersection Observer
PlaceholdersNone vs Color vs BlurBlur-up for visual continuity
StateLocal vs Context vs ReduxContext for simple; Redux for complex filtering