From harness-claude
Implements media lazy loading: native `loading='lazy'` for images/iframes, video poster optimization, LQIP, BlurHash, progressive rendering, above-fold prioritization. Fixes Lighthouse offscreen image issues.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Master media lazy loading strategies — native browser lazy loading for images and iframes, video poster optimization, low-quality image placeholders (LQIP), BlurHash encoding, progressive rendering techniques, and prioritization of above-the-fold media.
Implements lazy loading with Intersection Observer for visibility triggers, code splitting, progressive hydration, and virtual scrolling to reduce initial payload for below-the-fold content and heavy lists.
Generates code and provides guidance for lazy loading implementations in React, Vue, and CSS frontend projects. Activates on lazy loading implementer queries for performance and best practices.
Optimizes React performance via Profiler setup, memoization (React.memo, useMemo, useCallback), code splitting (React.lazy), list virtualization (react-window), useTransition/useDeferredValue, bundle analysis, and Web Vitals tracking.
Share bugs, ideas, or general feedback.
Master media lazy loading strategies — native browser lazy loading for images and iframes, video poster optimization, low-quality image placeholders (LQIP), BlurHash encoding, progressive rendering techniques, and prioritization of above-the-fold media.
Use native lazy loading for below-fold images. The loading="lazy" attribute defers image loading until the image approaches the viewport:
<!-- Below-fold images: lazy load -->
<img
src="product.jpg"
alt="Product photo"
width="400"
height="300"
loading="lazy"
decoding="async"
/>
<!-- Above-fold / LCP images: NEVER lazy load -->
<img src="hero.jpg" alt="Hero banner" width="1200" height="600" fetchpriority="high" />
The browser determines the loading threshold (typically 1250-2500px from viewport depending on connection speed). Do not add loading="lazy" to the LCP image or any image visible in the initial viewport.
Lazy load iframes for video embeds. YouTube and Vimeo iframes load 500KB+ of JavaScript. Defer until the user indicates intent:
<!-- Native iframe lazy loading -->
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
loading="lazy"
width="560"
height="315"
title="Video title"
allow="accelerometer; autoplay; encrypted-media"
allowfullscreen
></iframe>
<!-- Better: facade pattern — show thumbnail, load iframe on click -->
function YouTubeFacade({ videoId, title }: { videoId: string; title: string }) {
const [loaded, setLoaded] = useState(false);
if (loaded) {
return (
<iframe
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
width="560"
height="315"
title={title}
allow="accelerometer; autoplay; encrypted-media"
allowfullscreen
/>
);
}
return (
<button
onClick={() => setLoaded(true)}
style={{ position: 'relative', width: 560, height: 315 }}
aria-label={`Play: ${title}`}
>
<img
src={`https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`}
alt={title}
width="560"
height="315"
loading="lazy"
/>
<PlayIcon />
</button>
);
}
Implement LQIP (Low-Quality Image Placeholders). Show a tiny blurred preview while the full image loads:
// Generate LQIP at build time with sharp
const sharp = require('sharp');
async function generateLQIP(inputPath) {
const buffer = await sharp(inputPath)
.resize(20) // 20px wide
.blur(2)
.jpeg({ quality: 20 })
.toBuffer();
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
// ~200-400 bytes, inlineable in HTML
}
<!-- LQIP placeholder with fade-in transition -->
<div class="image-container" style="aspect-ratio: 4/3;">
<img
src="data:image/jpeg;base64,/9j/4AAQSkZJ..."
data-src="product-full.jpg"
alt="Product"
class="lqip-image"
width="800"
height="600"
/>
</div>
<style>
.image-container {
overflow: hidden;
}
.lqip-image {
width: 100%;
height: 100%;
object-fit: cover;
filter: blur(20px);
transform: scale(1.1);
transition:
filter 0.3s,
transform 0.3s;
}
.lqip-image.loaded {
filter: blur(0);
transform: scale(1);
}
</style>
Use BlurHash for compact placeholders. BlurHash encodes a placeholder in ~20-30 characters, decoded to a blurred preview on the client:
// Server: encode at upload/build time using sharp + blurhash
import { encode } from 'blurhash';
import sharp from 'sharp';
async function generateBlurHash(imagePath: string): Promise<string> {
const { data, info } = await sharp(imagePath)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
return encode(new Uint8ClampedArray(data), info.width, info.height, 4, 3);
}
// Client: decode hash to pixels, render to <canvas> via ctx.putImageData()
Implement video poster optimization. For video elements, use a poster image to avoid loading video data before play:
<video poster="video-poster.webp" preload="none" width="1280" height="720" playsinline>
<source src="video.mp4" type="video/mp4" />
</video>
Set preload="none" to prevent the browser from downloading any video data. The poster image provides the visual preview. Combine with loading="lazy" on a facade image for below-fold videos.
Implement progressive image loading with CSS transitions. Preload the full image in a new Image(), swap src on load, and animate with filter: blur(20px) transitioning to filter: none over 300ms. This smooths the placeholder-to-image transition without layout shift.
Configure the loading threshold. Browsers load lazy images before they enter the viewport. Chrome uses a distance threshold that varies by connection speed (~1250px on 4G, ~2500px on slow 3G). For custom Intersection Observer implementations, set rootMargin based on image size and expected scroll speed:
// Small thumbnails (fast to load): trigger closer
const observer = new IntersectionObserver(callback, { rootMargin: '200px' });
// Large hero images (slow to load): trigger earlier
const observer = new IntersectionObserver(callback, { rootMargin: '500px' });
When loading="lazy" is set, the browser defers the image fetch until the image is within a distance threshold of the viewport. This threshold is not configurable by developers — it varies by browser and connection speed. Chrome on a fast connection starts loading images ~1250px before they enter the viewport. On slow 3G, this increases to ~2500px to compensate for longer download times. Images with loading="lazy" that are in the initial viewport on page load are loaded immediately (no deferral).
Medium inlines a ~200-byte blurred LQIP as a base64 data URI. When the container approaches the viewport (Intersection Observer, 300px rootMargin), the full image loads and a CSS transition fades from blur to sharp over 300ms. Result: zero layout shift, immediate visual feedback, and ~95% of image bytes deferred.
Each API response includes a 28-character BlurHash string rendered to a canvas as an instant placeholder. The actual image loads progressively (headers first, then increasing resolution scans). Result: colored blurred previews within 50ms of scrolling, zero layout shift, and perceived-instant loading.
Lazy loading the LCP image. Adding loading="lazy" to the hero image or any image visible without scrolling delays LCP by the intersection observer threshold plus download time. The LCP image should be eagerly loaded with fetchpriority="high".
Using JavaScript lazy loading when native suffices. Libraries like lazysizes add JavaScript overhead for functionality the browser provides natively. Use loading="lazy" for standard cases. Reserve JavaScript solutions for LQIP, BlurHash, or Intersection Observer patterns that native lazy loading cannot provide.
Placeholders that cause layout shift. A placeholder with different dimensions than the final image causes Cumulative Layout Shift when swapped. Always set width, height, and aspect-ratio on containers. Placeholders must match the final image's aspect ratio.
Lazy loading all images indiscriminately. Applying loading="lazy" to every image on the page, including above-the-fold images, delays rendering of critical content. Audit which images are in the initial viewport and exclude them from lazy loading.
loading="lazy" and above-fold images do not.