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