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)

See demo

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 Blob objects
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-hidden handling
  • 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
  • inert instead 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.

Back to Home