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>Recommended Breakpoints
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
Recommended Libraries
Lightbox
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
MeshBasicMaterialfor 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; // GBMemory 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.mdreports/web-photography-portfolios.md
External resources:
See Also
- Static Site Hosting — deployment patterns for static sites
- Visual Practice — creating visualizations and diagrams for web content
- API Sync Pattern — data collection patterns for web applications
Footnotes
-
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. ↩
-
Browser support for
fetchpriorityattribute 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. ↩ -
PhotoSwipe v5 documentation and feature comparison: https://photoswipe.com (verified accessible 2026-02-26). MIT licensed, actively maintained as of 2026. ↩
-
Sharp image processing library: https://sharp.pixelplumbing.com (verified accessible 2026-02-26). Performance benchmarks vs ImageMagick documented in official GitHub repository. ↩