Practical patterns for modern web development, focused on performance, accessibility, and modern browser features.

Image Optimization

Modern Image Formats

AVIF → WebP → JPEG is the recommended fallback chain for 2025+:

  • AVIF: 50% smaller than JPEG, 20% smaller than WebP
  • WebP: Broad support, good compression
  • JPEG: Universal fallback

Browser support (Baseline 2024):1

  • AVIF: Chrome 85+, Firefox 93+, Safari 16+
  • WebP: Universal
  • JPEG XL: Safari-only, Chromium pending
<picture>
  <source type="image/avif" srcset="photo-800.avif 800w, photo-1600.avif 1600w, photo-2400.avif 2400w" sizes="100vw">
  <source type="image/webp" srcset="photo-800.webp 800w, photo-1600.webp 1600w, photo-2400.webp 2400w" sizes="100vw">
  <img src="photo-1600.jpg" 
       srcset="photo-800.jpg 800w, photo-1600.jpg 1600w, photo-2400.jpg 2400w" 
       sizes="100vw" 
       alt="Description" 
       width="1600" 
       height="1067" 
       loading="lazy">
</picture>

For photography and high-resolution imagery:

  • 640px — Mobile portrait
  • 1024px — Tablet/small desktop
  • 1600px — Desktop
  • 2400px — Retina/4K
  • 2560px — Practical maximum (diminishing returns beyond)

Image Processing

Sharp (Node.js) is 4-5× faster than ImageMagick and powers most SSG image plugins.

For static sites: generate at build time rather than using CDN transformation. Eliminates ongoing costs and works offline.

CDN services (Cloudinary, Imgix, ImageKit) are worthwhile only for:

  • User-uploaded content
  • Galleries exceeding thousands of images
  • When build times become problematic

Native CSS Features

Lazy Loading

Native loading="lazy" reached universal modern browser coverage in 2024:

<img src="photo.jpg" loading="lazy" alt="Description" width="800" height="600">

Critical exception: Never lazy-load LCP (Largest Contentful Paint) images. Use fetchpriority="high" instead:

<img src="hero.jpg" fetchpriority="high" alt="Hero" width="1920" height="1080">

Browser support: Chrome 102+, Safari 17.2+, Firefox 132+2 Performance impact: 4-20% LCP improvement

Scroll-Snap for Native Carousels

CSS scroll-snap enables horizontal galleries without JavaScript:

.gallery {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}
 
.gallery > * {
  flex: 0 0 100vw;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

Add -webkit-overflow-scrolling: touch for iOS momentum scrolling.

When to add JavaScript:

  • Autoplay
  • Infinite looping
  • Thumbnail synchronization
  • Complex ARIA announcements
  • Analytics tracking

Default: Start with CSS scroll-snap, add JavaScript only for specific needs.

Layout Stability

Three properties prevent Cumulative Layout Shift (CLS):

img {
  aspect-ratio: 3 / 2;      /* Reserve space before load */
  object-fit: cover;        /* Crop to fill container */
  height: auto;             /* Maintain aspect ratio */
}

Always include width and height attributes in HTML:

<img src="photo.jpg" width="1600" height="1067" alt="Description">

Target: < 0.1 CLS for Core Web Vitals

PhotoSwipe v53 (MIT license)

  • 15% smaller than v4
  • ES modules
  • Exceptional touch gestures
  • Spring-based physics animations
  • Best for: Advanced zoom, programmatic control

GLightbox (~11KB, MIT license)

  • Lighter alternative
  • Video support built-in
  • Best for: Simplicity over features

Avoid: lightGallery, Fancybox (require commercial licenses)

Carousels

Embla Carousel (~6KB, MIT)

  • Best performance-to-size ratio
  • Physics-based animations
  • Excellent swipe precision

Swiper (MIT)

  • 20+ modules
  • CSS scroll-snap mode available
  • Best for: Feature breadth

Image Processing

Sharp4 (Node.js)

  • 4-5× faster than ImageMagick
  • Powers most SSG image plugins
  • Memory-efficient for batch processing

Static Site Generators

For photography portfolios and content-heavy sites:

Eleventy — Most flexible, @11ty/eleventy-img plugin
Hugo — Fastest builds, built-in Go image processing
Astro — Islands architecture, modern tooling, partial hydration

All three generate responsive images automatically at build time.

Three.js + Physics Integration

For interactive 3D experiences (e.g., physics-based photo galleries):

Rapier3D over Rapier2D for Three.js integration:

  • Direct 1:1 transform mapping
  • Proper stacking, tilting, gravity-based settling
  • Performance: 100-200 bodies @ 60fps desktop, 30-50 @ 30fps mobile
import RAPIER from '@dimforge/rapier3d-compat';
await RAPIER.init();
 
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
 
// Sync physics to render
for (const [mesh, body] of dynamicBodies) {
  mesh.position.copy(body.translation());
  mesh.quaternion.copy(body.rotation());
}

Mobile WebGL Constraints

iOS Safari crashes without warning when exceeding ~1.4GB memory.

Key optimizations:

  • Cap pixel ratio at 1.0 (saves 4× fill rate vs 2.0)
  • Disable shadows or use BasicShadowMap @ 256px
  • Limit physics timestep to 30Hz (vs 60Hz desktop)
  • Use MeshBasicMaterial for distant objects
  • Implement on-demand rendering when scene is static

Device detection:

const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
const isLowEnd = navigator.deviceMemory <= 2; // GB

Memory management requires explicit disposal:

function disposePhoto(mesh) {
  mesh.geometry.dispose();
  mesh.material.map?.dispose();
  mesh.material.dispose();
  scene.remove(mesh);
}

Touch Gesture Handling

For distinguishing drag vs pan vs zoom:

function resolveGesture(touchCount, hitObjects) {
  if (touchCount === 1) {
    return hitObjects.length > 0 ? 'DRAG_PHOTO' : 'PAN_CANVAS';
  }
  if (touchCount === 2) {
    // Both fingers on same object = rotate, otherwise = zoom
    return (hitObjects[0] === hitObjects[1]) ? 'TWO_FINGER_ROTATE' : 'PINCH_ZOOM';
  }
}

Click vs drag detection (5-10px threshold):

const DRAG_THRESHOLD = 5;
let startPos, isDragging = false;
 
canvas.addEventListener('pointerdown', e => {
  startPos = { x: e.clientX, y: e.clientY };
});
 
canvas.addEventListener('pointermove', e => {
  const dist = Math.hypot(e.clientX - startPos.x, e.clientY - startPos.y);
  if (dist > DRAG_THRESHOLD) isDragging = true;
});

Use touch-action: none on canvas to prevent browser gesture interference.

Performance Targets

Photography portfolios:

  • Image size: 550-875KB per image
  • Resolution: 1860×1140px (standard), 2500×1700px (4K/5K)
  • Color profile: sRGB (modern browsers respect embedded profiles)

Mobile texture budgets:

  • iOS Safari: ~200-300MB total texture memory
  • Exceeding causes crashes without warning

Three.js performance:

  • Desktop: 60fps with shadows
  • Mobile: 30fps, shadows disabled or BasicShadowMap

Footnotes

References

Full technical reports in agent/artifacts:

  • reports/threejs-rapier-photo-gallery.md
  • reports/web-photography-portfolios.md

External resources:

See Also

Footnotes

  1. Browser support for modern image formats verified via caniuse.com and MDN Web Docs as of February 2026. AVIF support: Chrome 85+ (August 2020), Firefox 93+ (October 2021), Safari 16+ (September 2022). WebP has universal modern browser support since 2020.

  2. Browser support for fetchpriority attribute tracked via MDN Web Docs Browser Compatibility Data. Chrome 102+ (May 2022), Safari 17.2+ (December 2023), Firefox 132+ (October 2024). Performance impact data from Chrome team’s Core Web Vitals case studies.

  3. PhotoSwipe v5 documentation and feature comparison: https://photoswipe.com (verified accessible 2026-02-26). MIT licensed, actively maintained as of 2026.

  4. Sharp image processing library: https://sharp.pixelplumbing.com (verified accessible 2026-02-26). Performance benchmarks vs ImageMagick documented in official GitHub repository.