| name | wsh-image-optimization |
| description | Image optimization for WSH 2026 — AVIF/WebP conversion, image resizing, GIF-to-video conversion, lazy loading, and CoveredImage component refactoring. |
WSH: Image Optimization
The public directory contains 87.9MB of images and 183.7MB of GIFs. Images are loaded via jQuery sync XHR and processed client-side with image-size and piexifjs libraries.
Use this skill when optimizing image loading, converting formats, or refactoring image components.
Techniques
1. Convert Images to AVIF/WebP
Using Sharp (server-side build script):
npx sharp-cli --input public/images/*.jpg --output public/images/ --format avif --quality 50
Or programmatically:
const sharp = require("sharp");
await sharp("input.jpg").avif({ quality: 50 }).toFile("output.avif");
AVIF quality 50 ≈ JPEG quality 80 visually, with 60-70% size reduction.
2. Simplify CoveredImage Component
The current component:
- Fetches image as binary (sync XHR via jQuery)
- Parses with image-size library to get dimensions
- Reads EXIF with piexifjs for orientation
- Creates blob URL
Replace with simple <img> tag:
<img src={src} alt="" loading="lazy" style={{ objectFit: "cover" }} />
Get dimensions from server API or use CSS aspect-ratio.
3. GIF to Video Conversion
Convert GIFs to MP4/WebM at build time:
ffmpeg -i input.gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" output.mp4
Replace <canvas> GIF rendering with <video autoplay loop muted playsinline>.
4. Lazy Loading
<img loading="lazy" src={src} />
For LCP images (first visible), use loading="eager" and fetchpriority="high".
VRT Risks
| Change | Visual Impact | Mitigation |
|---|
| AVIF conversion | Slight quality difference | Use quality 50+ and compare |
| CoveredImage simplification | Aspect ratio may differ | Match original dimensions with CSS |
| GIF to video | Animation timing may differ | Ensure autoplay and loop match |
Project-Specific Notes
- Images in
application/public/images/
- GIFs in
application/public/movies/ (183.7MB total)
- CoveredImage at
client/src/components/foundation/CoveredImage.tsx
- PausableMovie at
client/src/components/foundation/PausableMovie.tsx
- Images are loaded through
useFetch(src, fetchBinary) → sync XHR
- After jQuery removal, images will load async — but component still does unnecessary processing