PatternsCore Optimizations
Asset Optimization
Optimize images, fonts, and videos for maximum performance
Asset Optimization
Assets (images, fonts, videos) typically account for 50-70% of page weight. Optimizing them is critical for performance.
Image Optimization
1. Choose the Right Format
JPEG: Photos, complex images
PNG: Transparency, logos, graphics
WebP: Modern format, 25-35% smaller than JPEG/PNG
AVIF: Next-gen format, 50% smaller than JPEG (limited support)
SVG: Icons, logos, simple graphics2. Responsive Images
// Serve different sizes based on viewport
<img
srcSet="
/image-320w.webp 320w,
/image-640w.webp 640w,
/image-1280w.webp 1280w,
/image-1920w.webp 1920w
"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
src="/image-1280w.webp"
alt="Description"
loading="lazy"
/>3. Modern Image Formats with Fallback
<picture>
<source srcSet="/image.avif" type="image/avif" />
<source srcSet="/image.webp" type="image/webp" />
<img src="/image.jpg" alt="Description" />
</picture>4. Next.js Image Component
import Image from 'next/image';
// Automatic optimization, lazy loading, responsive
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // LCP image - load immediately
quality={85} // Default: 75
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
// Remote images
<Image
src="https://cdn.example.com/image.jpg"
alt="Remote image"
width={800}
height={400}
loader={({ src, width, quality }) => {
return `${src}?w=${width}&q=${quality || 75}`;
}}
/>5. Image Compression
# Sharp (Node.js)
npm install sharp
# Compress JPEG
sharp('input.jpg')
.jpeg({ quality: 85, mozjpeg: true })
.toFile('output.jpg');
# Convert to WebP
sharp('input.jpg')
.webp({ quality: 80 })
.toFile('output.webp');
# Convert to AVIF
sharp('input.jpg')
.avif({ quality: 70 })
.toFile('output.avif');
# Resize
sharp('input.jpg')
.resize(1280, 720, { fit: 'cover' })
.toFile('output.jpg');6. Lazy Loading
// Native lazy loading (modern browsers)
<img src="/image.jpg" loading="lazy" alt="Description" />
// Intersection Observer (more control)
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '50px' } // Start loading 50px before visible
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={loaded ? src : undefined}
alt={alt}
style={{ opacity: loaded ? 1 : 0 }}
/>
);
}7. Image CDN
// Cloudinary example
const imageUrl = (publicId: string, transformations: string) => {
return `https://res.cloudinary.com/demo/image/upload/${transformations}/${publicId}`;
};
// Usage
<img
src={imageUrl('sample', 'w_800,h_600,c_fill,q_auto,f_auto')}
alt="Optimized"
/>
// Imgix example
const imgixUrl = (src: string, params: Record<string, string>) => {
const query = new URLSearchParams(params).toString();
return `https://demo.imgix.net/${src}?${query}`;
};
<img
src={imgixUrl('image.jpg', { w: '800', h: '600', fit: 'crop', auto: 'format,compress' })}
alt="Optimized"
/>8. Progressive JPEG
# ImageMagick
convert input.jpg -interlace Plane output.jpg
# Sharp
sharp('input.jpg')
.jpeg({ progressive: true, quality: 85 })
.toFile('output.jpg');Font Optimization
1. Font Loading Strategy
/* font-display strategies */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
/*
swap: Show fallback immediately, swap when font loads (best for most)
optional: Only use font if cached (best for performance)
fallback: Show fallback briefly (~100ms), swap if loaded quickly
block: Wait for font (~3s), then show fallback (avoid)
*/
}2. Subset Fonts
# Glyphhanger - subset to only used characters
npx glyphhanger https://example.com --formats=woff2 --subset=fonts/font.ttf
# Manual subset (Latin only)
npx glyphhanger --subset=fonts/font.ttf --latin3. Variable Fonts
/* Single file, all weights */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900; /* All weights from 100-900 */
font-display: swap;
}
/* Use any weight */
h1 {
font-weight: 650; /* Exact weight between 600 and 700 */
}4. Preload Critical Fonts
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
);
}5. Next.js Font Optimization
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}6. System Fonts
/* Use system fonts - instant, no download */
body {
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
}Video Optimization
1. Choose the Right Format
H.264 (MP4): Universal support, good compression
H.265 (HEVC): Better compression (50%), limited support
VP9: Google's format, YouTube uses it
AV1: Next-gen, best compression (30% better than VP9), growing support2. Lazy Load Videos
function LazyVideo({ src, poster }: { src: string; poster: string }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && videoRef.current) {
const video = videoRef.current;
video.src = src;
video.load();
setLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '200px' }
);
if (videoRef.current) {
observer.observe(videoRef.current);
}
return () => observer.disconnect();
}, [src]);
return (
<video
ref={videoRef}
poster={poster}
controls
preload="none"
muted
playsInline
/>
);
}3. Adaptive Streaming
// HLS (HTTP Live Streaming)
<video controls>
<source src="/video/playlist.m3u8" type="application/x-mpegURL" />
</video>
// DASH (Dynamic Adaptive Streaming)
<video controls>
<source src="/video/manifest.mpd" type="application/dash+xml" />
</video>
// Use video.js for better compatibility
import VideoJS from 'video.js';
import 'video.js/dist/video-js.css';
<video-js
data-setup='{"fluid": true}'
controls
preload="auto"
>
<source src="/video/playlist.m3u8" type="application/x-mpegURL" />
</video-js>4. Animated Content: Video > GIF
GIF: 2-3 MB
MP4: 200-300 KB (10x smaller!)
Use <video> instead of GIF for animations// Replace GIF with video
<video
autoPlay
loop
muted
playsInline
style={{ width: '100%', height: 'auto' }}
>
<source src="/animation.mp4" type="video/mp4" />
<source src="/animation.webm" type="video/webm" />
</video>5. Cloudflare Stream / Mux
// Cloudflare Stream
<iframe
src="https://iframe.videodelivery.net/VIDEO_ID"
style={{ border: 'none', position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
allowFullScreen
/>
// Mux
import MuxPlayer from '@mux/mux-player-react';
<MuxPlayer
playbackId="PLAYBACK_ID"
metadata={{
video_title: "Video Title",
viewer_user_id: "user-id",
}}
streamType="on-demand"
poster="/poster.jpg"
/>Best Practices
Images
- WebP/AVIF: Use modern formats with fallbacks
- Responsive: Serve appropriate sizes
- Lazy Load: Load images as needed
- Compress: 80-85% quality is usually fine
- CDN: Use image CDN for automatic optimization
- Dimensions: Always specify width/height to prevent layout shift
- Priority: Mark LCP images with
priority
Fonts
- Subset: Remove unused characters
- Variable Fonts: One file, all weights
- font-display: swap: Prevent invisible text
- Preload: Only preload critical fonts
- Limit: 2-3 font families maximum
- System Fonts: Consider for performance
Videos
- Lazy Load: Load when visible
- Adaptive Streaming: HLS/DASH for long videos
- Compress: Use H.265/VP9/AV1
- Poster: Always include a poster image
- Video > GIF: Replace GIFs with videos
- CDN: Use video CDN (Cloudflare, Mux)
- Preload:
preload="none"for below-fold videos
Measurement
// Measure asset transfer sizes
performance.getEntriesByType('resource').forEach(entry => {
if (entry.initiatorType === 'img' || entry.initiatorType === 'video') {
console.log(entry.name, `${(entry.transferSize / 1024).toFixed(2)} KB`);
}
});
// Check font loading
document.fonts.ready.then(() => {
console.log('All fonts loaded');
document.fonts.forEach(font => {
console.log(font.family, font.weight, font.status);
});
});Optimizing assets is the fastest way to improve page load performance—start here first!