Dynamic Photo Gallery
by Kuligaposten Oct 23 2024
This post explains how the frontend gallery works and why each architectural decision was made.
Building a High-Performance Front-End Photo Gallery (No Framework)
This project started with a simple goal:
Build a fast, accessible, framework-free photo gallery that scales to thousands of images.
No React. No Vue. No virtual DOM.
Just modern browser APIs, Web Workers, and CSS.
This post explains how the frontend gallery works and why each architectural decision was made.
Core Requirements
The gallery needed to:
- Load thousands of images
- Stay responsive on low-end devices
- Avoid layout shifts
- Support masonry layout
- Include a keyboard-accessible lightbox
- Support slideshows
- Work with a static HTML build
Performance and simplicity were more important than abstractions.
Data Comes From JSON
The frontend does not know anything about folders.
It consumes pure JSON:
{
"images": [
{
"src": "...",
"thumb": "...",
"alt": "...",
"date": "...",
"bigWidth": 2048,
"bigHeight": 1365
}
]
}
This JSON is generated by:
- Node + Express or
- PHP backend
The frontend is backend-agnostic.
Masonry Grid (Pure CSS)
The gallery uses CSS columns instead of JavaScript layout engines.
.masonry {
column-count: 4;
column-gap: 1rem;
}
@media (max-width: 1024px) {
.masonry {
column-count: 3;
}
}
@media (max-width: 768px) {
.masonry {
column-count: 2;
}
}
Why CSS Columns?
- Zero JavaScript cost
- No reflows on resize
- Naturally responsive
- Works perfectly with lazy loading
The trade-off: navigation order flows top-to-bottom, not left-to-right — a compromise worth making.
Intersection Observer for Animation & Lazy Reveal
Images fade in when they enter the viewport:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
}
});
});
This avoids:
- Scroll event listeners
- Forced layouts
- Main-thread jank
Why Web Workers Are Critical
Downloading hundreds of images on the main thread causes:
- Jank
- Input lag
- Slow interaction
So all image fetching happens inside a Web Worker.
Worker responsibilities
- Download thumbnails
- Download large images
- Return
Blobobjects
const blob = await fetch(src).then((r) => r.blob());
The main thread never blocks.
Object URLs Instead of Network URLs
Images are rendered using:
URL.createObjectURL(blob);
Benefits:
- No refetching
- No cache thrashing
- Instant reuse in lightbox
- Full control over memory
URLs are revoked when no longer needed:
URL.revokeObjectURL(url);
DocumentFragment for Rendering
Rendering is batched using DocumentFragment:
const fragment = document.createDocumentFragment();
items.forEach((item) => fragment.appendChild(node));
container.appendChild(fragment);
This avoids repeated DOM reflows and keeps rendering fast even with hundreds of items.
Dynamic Album Loading
Albums are lazy-loaded on demand:
- Album list fetched once
- Images fetched only when clicked
- Last album saved in
localStorage
This allows instant startup with zero images loaded.
Lightbox Architecture
The lightbox is fully custom and framework-free.
Features
- Keyboard navigation
- Focus trapping
inert+aria-hiddenhandling- Blob-based images
- Preloading next image
currentBigURL = URL.createObjectURL(item.bigBlob);
lightboxImg.src = currentBigURL;
No network requests happen inside the lightbox.
Slideshow Inside the Lightbox
The slideshow is a simple state machine:
setInterval(() => {
index = (index + 1) % images.length;
show();
}, 4000);
Design decisions
- Slideshow does not auto-start
- Stops on manual navigation
- Keyboard controllable
- Single play/pause button
- Accessible
aria-pressed
Accessibility Decisions
The gallery follows real accessibility, not checkbox compliance:
- Focus restored on close
inertinstead of hiding focused elements- Keyboard shortcuts
- Buttons instead of divs
- No forced focus traps
This avoids the common Bootstrap modal warnings and screen reader issues.
Why No Framework?
Frameworks shine for stateful UI.
This gallery is:
- Mostly read-only
- Event-driven
- Performance-sensitive
Using vanilla JS:
- Reduces bundle size to near zero
- Avoids hydration costs
- Makes behavior explicit
- Keeps control of memory
Performance Characteristics
- Zero JS layout work
- Worker-offloaded downloads
- No runtime dependencies
- Reusable blobs
- Minimal memory leaks
It performs well even with thousands of images.
The Result
- Static HTML frontend
- Tiny backend API
- Lightroom → upload → live gallery
- Masonry grid
- Lightbox + slideshow
- No console errors
- No framework lock-in
This is a modern web app built the old-school way — deliberately.
Final Thoughts
Sometimes the best architecture is:
Less abstraction, more intent.
This gallery proves that modern browser APIs are powerful enough to build rich, accessible, high-performance applications without a framework.
And that’s a good thing.