Next.js optimizes images automatically with next/image. Video, however, doesn't get the same built-in optimization treatment. There is no next/video, and no native support for compression, format negotiation, or adaptive streaming.
This guide walks through a complete video optimization pipeline in Next.js, covering format conversion, compression, resizing, lazy loading with scroll-aware playback, and adaptive bitrate streaming.
We'll implement all of this using ImageKit's real-time video processing API via its easy-to-use Next.js SDK. So there's no separate processing pipeline or build step required.
What we'll cover:
- Setting up the ImageKit Next.js SDK.
- Resizing, compressing, and converting video formats.
- Generating poster images and lazy loading for better Core Web Vitals.
- Adaptive bitrate streaming with HLS for bandwidth-aware delivery.
The complete working code is in the nextjs-video-optimization repository.
Setting up the ImageKit SDK
Install the package:
npm install @imagekit/nextWrap your root layout with ImageKitProvider:
// app/layout.tsx
import { ImageKitProvider } from '@imagekit/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ImageKitProvider urlEndpoint={process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!}>
{children}
</ImageKitProvider>
</body>
</html>
);
}Add your URL endpoint to .env.local:
NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT=https://ik.imagekit.io/ikmediaImport the Video component from the SDK wherever you need it:
import { Video } from '@imagekit/next';The Video component is a lightweight wrapper around the HTML <video> element. It supports all standard HTML video attributes (autoPlay, muted, loop, controls, poster, preload, playsInline, and more) plus ImageKit-specific props: urlEndpoint, transformation, transformationPosition, and queryParameters.
Resize to match your viewport
The single biggest optimization is serving video at the size you actually display it. A 4K source video rendered in a 1280px container wastes bandwidth on pixels nobody sees.
Add a width to the transformation prop to resize server-side before delivery:
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280 }]}
autoPlay
muted
playsInline
loop
preload="none"
className="w-full h-full object-cover"
/>ImageKit maintains the aspect ratio automatically when you specify only width. The original stays untouched in your media library.
The difference is dramatic:
| Metric | Original (4K) | Resized (1280px) | Improvement |
|---|---|---|---|
| File Size | 63.76 MB | 3.2 MB | 95% smaller |
| Load Time (4G) | 5.3s | 420ms | 92% faster |
When you specify both width and height, the default crop mode is maintain_ratio, which crops from all sides to preserve the aspect ratio. If you want the full video to remain visible without any cropping, specify a different crop strategy:
// Preserves the full video, may resize one dimension to maintain aspect ratio
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280, height: 720, crop: 'at_max' }]}
/>
// Preserves the full video by adding padding to match exact dimensions
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280, height: 720, cropMode: 'pad_resize' }]}
/>Note: The crop parameter controls the basic resize strategy (maintain_ratio, force, at_max, at_least), while cropMode controls the crop method (extract, pad_resize). See the video resize and crop docs for the full list of options.
Format, quality, and audio
Three more optimizations that stack on top of resizing.
Automatic format conversion
ImageKit automatically delivers the most efficient video format based on browser support. Browsers like Chrome receive VP9/WebM for better compression, while Safari gets H.264/MP4. The transformation happens server-side - your URL stays the same, no <source> tag juggling required.
Open DevTools and check the Content-Type response header on any ImageKit video request. You'll see video/webm in Chrome even though the URL ends in .mp4. This automatic format negotiation ensures optimal file size without additional code.
Quality control
ImageKit compresses video automatically. The default quality setting is configurable in your dashboard settings. Override it per-URL when you need more control:
// Higher quality for product videos
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280, quality: 80 }]}
/>
// Lower quality for ambient backgrounds
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280, quality: 40 }]}
/>For most use cases, the default works well. Adjust only when visual fidelity matters (product demos) or when file size is critical (mobile hero backgrounds).
Strip audio for decorative videos
Muted videos still carry an audio track. Stripping it saves 10-30% of the file size:
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280, audioCodec: 'none' }]}
autoPlay
muted
loop
playsInline
/>Use audioCodec: 'none' on any video where audio will never play: hero backgrounds, ambient loops, decorative sections.
Common video transformations
Here are the most useful video transformation options. For the complete list, see the Next.js SDK supported transformations reference.
| SDK Property | URL Parameter | What it does |
|---|---|---|
width | w | Resize to specified width in pixels |
height | h | Resize to specified height in pixels |
quality | q | Set output quality (1-100) |
audioCodec | ac | Set audio codec (none strips audio) |
videoCodec | vc | Set video codec (e.g., h264) |
startOffset | so | Start the video at a specific timestamp (in seconds) |
endOffset | eo | End the video at a specific timestamp (in seconds) |
duration | du | Limit the video duration (in seconds) |
crop | c | Crop strategy (maintain_ratio, force, at_max, at_least) |
cropMode | cm | Crop method (pad_resize, extract) |
focus | fo | Focus point for cropping (e.g., center, top, left) |
streamingResolutions | sr | Resolutions for adaptive bitrate streaming (e.g., 240_360_480_720) |
These properties can be combined in a single transformation object. For the full reference, see the Next.js SDK docs.
Generate poster images
A poster image prevents the black flash before a video loads. Since Chrome 116, poster images are also LCP candidates, which means they directly affect your Largest Contentful Paint score.
ImageKit generates a thumbnail from any video by appending /ik-thumbnail.jpg to the path:
https://ik.imagekit.io/ikmedia/docs_images/examples/Videos/example_video_2.mp4/ik-thumbnail.jpgBy default this extracts the first frame. Use the startOffset parameter to pick a specific timestamp, typically past any black opening frames.
In the SDK, use buildSrc to generate the poster URL alongside your video:
import { Video, buildSrc } from '@imagekit/next';
const urlEndpoint = process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!;
const poster = buildSrc({
urlEndpoint,
src: '/docs_images/examples/Videos/example_video_2.mp4/ik-thumbnail.jpg',
transformation: [{ width: 1280, startOffset: 3 }],
});
<Video
src="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 1280 }]}
poster={poster}
preload="none"
autoPlay
muted
playsInline
loop
className="w-full"
/>The thumbnail URL is a regular ImageKit image URL. You can apply image transformations to it: resize, convert to WebP, add blur for a placeholder. See the thumbnail docs for all options.
For above-the-fold videos, preload the poster to improve LCP:
// In your page's <head>
<link rel="preload" as="image" href={poster} fetchPriority="high" />A poster image loads in ~100-200ms. A video's first frame can take seconds.
Lazy load below-fold video
Loading six videos on page mount defeats every optimization above. Use the right preload strategy and load below-fold videos on scroll.
Preload strategies
| Value | What downloads | Best for |
|---|---|---|
preload="none" | Nothing until play | Below-fold videos with poster images |
preload="metadata" | Duration, dimensions, first frame | Above-fold user-initiated videos |
preload="auto" | Entire file (browser decides) | Critical autoplay hero videos only |
Mobile browsers often ignore preload="auto" to save bandwidth. Don't rely on it for critical playback.
Load on scroll with Intersection Observer
This component renders a poster image initially, then swaps to the full Video component when it enters the viewport:
// components/LazyVideo.tsx
'use client';
import { useState } from 'react';
import { Video, buildSrc } from '@imagekit/next';
import { useInView } from 'react-intersection-observer';
interface LazyVideoProps {
path: string;
transformation?: Array<Record<string, string | number>>;
className?: string;
}
export function LazyVideo({
path,
transformation = [{ width: 1280 }],
className = '',
}: LazyVideoProps) {
const urlEndpoint = process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT!;
const [hasEntered, setHasEntered] = useState(false);
const { ref } = useInView({
triggerOnce: true,
rootMargin: '200px',
onChange: (inView) => {
if (inView) setHasEntered(true);
},
});
const poster = buildSrc({
urlEndpoint,
src: `${path}/ik-thumbnail.jpg`,
transformation: [{ width: 1280 }],
});
return (
<div ref={ref} className={className}>
{hasEntered ? (
<Video
src={path}
transformation={transformation}
poster={poster}
preload="metadata"
controls
playsInline
className="h-full w-full object-cover"
/>
) : (
<img
src={poster}
alt=""
className="h-full w-full object-cover"
/>
)}
</div>
);
}Usage:
<LazyVideo
path="/docs_images/examples/Videos/example_video_2.mp4"
transformation={[{ width: 800 }]}
className="aspect-video"
/>The rootMargin: '200px' starts loading 200px before the component enters the viewport, giving the video a head start.
Adaptive bitrate streaming with HLS
A single MP4 file serves the same quality to a phone on 3G and a desktop on fiber. Adaptive bitrate streaming solves this by generating multiple quality tiers and letting the player switch between them based on network conditions.
Generate an HLS manifest with one URL
Append /ik-master.m3u8 to any video path and specify the resolutions you want:
https://ik.imagekit.io/ikmedia/docs_images/examples/Videos/example_video_2.mp4/ik-master.m3u8?tr=sr-240_360_480_720_1080ImageKit generates all resolution variants and caches them. Available tiers:
| Resolution | Max Bitrate | Output |
|---|---|---|
| 240p | 200K | 426x240 |
| 360p | 400K | 640x360 |
| 480p | 800K | 854x480 |
| 720p | 3300K | 1280x720 |
| 1080p | 5500K | 1920x1080 |
All variants use H.264 video and AAC audio.
First-request behavior
The first time you request an HLS manifest, ImageKit returns a 202 status with an empty body while it generates the variants. This can take seconds to minutes depending on video length and resolution count.
For production, request your manifest URLs once before going live, or use post-upload transformations to generate them eagerly on upload. You can listen for video webhook events (video.transformation.ready) to know when all variants are encoded.
Playing HLS in Next.js
Safari plays .m3u8 natively. Chrome, Firefox, and Edge need a JavaScript player to handle the manifest and segment loading. You have two good options.
Option 1: ImageKit Video Player (recommended)
The official @imagekit/video-player package handles HLS, adaptive bitrate streaming, quality switching, and cross-browser compatibility out of the box. It also includes built-in support for AI-generated subtitles, chapters, playlists, and shoppable videos. Check the official documentation for the latest features and updates.
Install the package:
npm install @imagekit/video-playerThen drop in the React component:
'use client';
import { IKVideoPlayer } from '@imagekit/video-player/react';
import '@imagekit/video-player/styles.css';
export default function VideoPlayer() {
const ikOptions = {
imagekitId: 'your_imagekit_id',
abs: {
protocol: 'hls',
sr: [360, 480, 720, 1080],
},
};
const source = {
src: 'https://ik.imagekit.io/your_imagekit_id/hero.mp4',
};
return <IKVideoPlayer ikOptions={ikOptions} source={source} />;
}That's the entire setup. The abs configuration tells the player to generate HLS variants at 360p, 480p, 720p, and 1080p, then handles quality switching automatically based on network conditions. You also get a quality selector in the player UI, seek thumbnails on the progress bar, and an optional floating mode when the player scrolls out of view.
Option 2: Video.js with VHS plugin
If you need a custom implementation and want full control over the player implementation, Video.js with the VHS (Video HTTP Streaming) plugin is an excellent choice. It provides robust HLS playback across all browsers with extensive customization options. For a complete implementation guide, see HLS streaming with Video.js and ImageKit.
Conclusion
Next.js gives you next/image for images but leaves video to you. Here is the optimization checklist:
- Resize to match the largest display size. Don't serve 4K to a 720px container.
- Let ImageKit auto-negotiate format. WebM for Chrome, MP4 for Safari.
- Strip audio with
audioCodec: 'none'on any video without meaningful sound. - Add a poster image and preload it for above-fold videos.
- Lazy load every below-fold video with Intersection Observer.
- Use HLS for long-form content where network conditions vary.
Upload once. ImageKit handles compression, format conversion, and CDN delivery.
Sign up for a free ImageKit account and try the width: 1280, audioCodec: 'none' transformation on your own video.