The Complete Guide to Video Optimization in Next.js

Resize, compress, strip audio, generate thumbnails, lazy load, and stream video in Next.js using ImageKit's URL-based transformations and Next.js SDK.

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:

  1. Setting up the ImageKit Next.js SDK.
  2. Resizing, compressing, and converting video formats.
  3. Generating poster images and lazy loading for better Core Web Vitals.
  4. 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/next

Wrap 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/ikmedia

Import 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:

MetricOriginal (4K)Resized (1280px)Improvement
File Size63.76 MB3.2 MB95% smaller
Load Time (4G)5.3s420ms92% 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 PropertyURL ParameterWhat it does
widthwResize to specified width in pixels
heighthResize to specified height in pixels
qualityqSet output quality (1-100)
audioCodecacSet audio codec (none strips audio)
videoCodecvcSet video codec (e.g., h264)
startOffsetsoStart the video at a specific timestamp (in seconds)
endOffseteoEnd the video at a specific timestamp (in seconds)
durationduLimit the video duration (in seconds)
cropcCrop strategy (maintain_ratio, force, at_max, at_least)
cropModecmCrop method (pad_resize, extract)
focusfoFocus point for cropping (e.g., center, top, left)
streamingResolutionssrResolutions 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.jpg

By 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

ValueWhat downloadsBest for
preload="none"Nothing until playBelow-fold videos with poster images
preload="metadata"Duration, dimensions, first frameAbove-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_1080

ImageKit generates all resolution variants and caches them. Available tiers:

ResolutionMax BitrateOutput
240p200K426x240
360p400K640x360
480p800K854x480
720p3300K1280x720
1080p5500K1920x1080

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.

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-player

Then 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:

  1. Resize to match the largest display size. Don't serve 4K to a 720px container.
  2. Let ImageKit auto-negotiate format. WebM for Chrome, MP4 for Safari.
  3. Strip audio with audioCodec: 'none' on any video without meaningful sound.
  4. Add a poster image and preload it for above-fold videos.
  5. Lazy load every below-fold video with Intersection Observer.
  6. 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.