How to Add a Video Background to a Website Without Killing Page Speed

A video background website looks great in a design mockup and terrible in a Lighthouse report, at least when it is built the lazy way. Drop a 40 MB MP4 into a hero section, hit publish, and you have just traded 1.5 seconds of Largest Contentful Paint for a bit of motion that most mobile users will never see.

It does not have to work that way. This is a practical, performance-first tutorial: the exact HTML5 markup, the encoding commands, the poster image strategy, the mobile fallback, the Express server headers, and the measurements that prove the whole thing did not hurt your Core Web Vitals. We will also cover the part most tutorials skip: when a looping video is the wrong tool and a CSS animation or an animated image is smarter. Dynamic Websites: 5 Excellent Examples of Video Backgrounds covers this in more depth.

What a video background website really costs

Before writing a line of code, put a number on the decision. A hero video is a bandwidth purchase, and you should know the price.

Approach Typical weight (10s loop) Risk to LCP
Unoptimised 1080p MP4 straight from the camera or stock site 20 MB to 60 MB Severe
Properly encoded H.264 MP4 + WebM, 1280px wide, no audio 700 KB to 2 MB Low if deferred
Animated GIF 5 MB to 20 MB Severe (blocking image decode)
CSS or SVG animation 2 KB to 30 KB None

Rule of thumb: the video file should never be the first thing the browser downloads, and it should never exceed roughly 2 MB for a desktop hero. If your creative concept cannot survive that budget, change the concept, not the budget.

website video background

Step 1: Decide whether you actually need video

Most of the sites you find in a “best video background websites” gallery use footage that carries real meaning: a product in motion, a place, a process, people. If your loop is an abstract gradient, drifting particles, or slow-moving smoke, you are paying megabytes for something a few lines of CSS can fake.

If the content is… Use Why
Real footage: product, location, people, manufacturing HTML5 <video> loop Only video reproduces real-world detail credibly
Abstract motion: gradients, blobs, grain, waves CSS animation, canvas or animated SVG Kilobytes instead of megabytes, resolution independent
Short 2 to 3 second UI demo Muted MP4 or animated AVIF/WebP Far smaller than GIF at the same quality
Anything on a page whose only job is conversion speed Static image Fastest paint, zero autoplay edge cases

And one hard rule: never ship a GIF for a background. A silent MP4 or WebM is roughly ten to twenty times smaller for the same visual result, and the browser can decode it on the GPU.

Step 2: Cut and prepare the source clip

Editing decisions matter more than encoder settings. Before you touch ffmpeg:

  1. Keep it between 6 and 12 seconds. Longer loops rarely get watched and multiply file size linearly.
  2. Choose low-motion footage. Slow pans, shallow depth of field and static camera positions compress dramatically better than handheld shots with fast movement or falling confetti.
  3. Strip the audio track entirely. A background video must be muted to autoplay anyway, so an audio track is pure waste.
  4. Make the loop seamless. Cut on a matching frame, or add a short cross-dissolve between the end and the start so the jump is invisible.
  5. Design for the overlay. If text sits over the video, pick footage with a calm area, then darken it with a CSS gradient so contrast ratios stay accessible.
  6. Check the licence. Free libraries such as Pexels, Pixabay, Coverr and Mixkit are fine for commercial use, but read the terms for logos, faces and trademarks visible in the frame.
website video background

Step 3: Encode the video properly

Two files cover the whole modern browser landscape: an MP4 (H.264) for universal support and a WebM (VP9 or AV1) that is typically 25 to 50 percent smaller where supported.

Recommended output targets

Setting Desktop hero Tablet / small viewport
Width 1600 to 1920 px 1280 px
Frame rate 24 to 25 fps 24 fps
Audio None None
File size ceiling 2 MB 1 MB
Pixel format yuv420p yuv420p

ffmpeg commands you can copy

H.264 MP4, with the moov atom moved to the front so playback can start before the file finishes downloading:

ffmpeg -i source.mov -an \
  -vf "scale=1600:-2,fps=25" \
  -c:v libx264 -profile:v high -pix_fmt yuv420p \
  -crf 27 -preset slow -g 50 \
  -movflags +faststart \
  hero-1600.mp4

VP9 WebM (broad support, good ratio):

ffmpeg -i source.mov -an \
  -vf "scale=1600:-2,fps=25" \
  -c:v libvpx-vp9 -crf 36 -b:v 0 -row-mt 1 \
  hero-1600.webm

AV1 WebM if your toolchain has SVT-AV1 (smallest files, slower encode):

ffmpeg -i source.mov -an \
  -vf "scale=1600:-2,fps=25" \
  -c:v libsvtav1 -crf 40 -preset 6 -pix_fmt yuv420p \
  hero-1600-av1.webm

Raise the CRF value until you see artefacts, then step back one. Because the video sits behind a dark overlay and moving text, you can usually push CRF several points higher than you would for a video the user is meant to study.

Generate the poster image from the first frame

ffmpeg -i hero-1600.mp4 -ss 00:00:00.5 -frames:v 1 poster.png
cwebp -q 72 poster.png -o hero-poster.webp
avifenc --min 24 --max 34 poster.png hero-poster.avif

Extracting the poster from the encoded video, not from the raw source, guarantees the still frame and the first video frame match. No flash, no colour shift when playback starts.

Step 4: The HTML5 markup

Every attribute below is there for a reason.

<section class="hero">
  <video
    class="hero__video"
    poster="/media/hero-poster.avif"
    muted
    loop
    playsinline
    preload="none"
    disablepictureinpicture
    aria-hidden="true"
    tabindex="-1">
    <source data-src="/media/hero-1600.webm" type="video/webm">
    <source data-src="/media/hero-1600.mp4" type="video/mp4">
  </video>

  <div class="hero__overlay"></div>

  <div class="hero__content">
    <h1>Ship your Express app faster</h1>
    <p>Minimal, unopinionated, production ready.</p>
    <a class="btn" href="/get-started">Get started</a>
  </div>
</section>

What each attribute does

  • muted: mandatory. Chrome, Safari and Firefox all block autoplay with sound.
  • playsinline: prevents iOS from opening the video full screen. Without it, your background becomes a takeover on iPhone.
  • loop: continuous playback, no manual restart logic.
  • preload=”none”: the browser fetches nothing until you tell it to. This is the single most important performance switch on the element.
  • poster: the still frame shown before and instead of playback. Serve it as AVIF or WebP.
  • data-src instead of src: the sources are inert until JavaScript activates them after the page has painted.
  • aria-hidden and tabindex=”-1″: decorative motion should be invisible to assistive tech and skipped by keyboard navigation.
  • Note that there is no autoplay attribute: we call play() from script once conditions are safe.

Step 5: CSS for a full-bleed hero that never shifts layout

.hero {
  position: relative;
  min-height: 70svh;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: #0b1020 url('/media/hero-poster.avif') center / cover no-repeat;
}

.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

.hero__overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(0,0,0,.60), rgba(0,0,0,.30));
}

.hero__content {
  position: relative;
  z-index: 2;
  color: #fff;
  text-align: center;
  max-width: 60ch;
  padding: 2rem;
}

/* Mobile fallback: poster only, no video download */
@media (max-width: 768px) {
  .hero__video { display: none; }
}

/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
}

Two details worth underlining. First, the poster is also set as a CSS background on the container, so there is never a blank frame between paint and playback. Second, the hero has an explicit minimum height, so nothing moves when the video element finally appears. That keeps Cumulative Layout Shift at zero. It is argued more carefully on pexels.com.

website video background

Step 6: Load the video after the page is usable

This script decides at runtime whether the visitor should get the video at all, then loads it only once the critical rendering work is finished.

const video = document.querySelector('.hero__video');

function shouldPlayBackgroundVideo() {
  if (!video) return false;

  const bigEnough = window.matchMedia('(min-width: 769px)').matches;
  const motionOk  = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const conn = navigator.connection || {};
  const saveData = conn.saveData === true;
  const slowNet  = /(^|-)2g$/.test(conn.effectiveType || '');

  return bigEnough && motionOk && !saveData && !slowNet;
}

function startBackgroundVideo() {
  video.querySelectorAll('source[data-src]').forEach(function (s) {
    s.src = s.dataset.src;
    s.removeAttribute('data-src');
  });
  video.load();
  const p = video.play();
  if (p && p.catch) p.catch(function () { /* poster stays visible, that is fine */ });
}

if (shouldPlayBackgroundVideo()) {
  window.addEventListener('load', function () {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(startBackgroundVideo, { timeout: 2000 });
    } else {
      setTimeout(startBackgroundVideo, 500);
    }
  });
}

What this buys you:

  • Phones and tablets receive a single optimised image, never the video bytes.
  • Data Saver users and 2G connections are excluded automatically.
  • The video competes with nothing during the critical path, so it cannot delay LCP.
  • If autoplay is refused, for example on iOS Low Power Mode, the promise rejection is caught and the poster simply stays on screen. No broken hero, no console noise.

Pause the video when it is off screen

A looping video that keeps decoding while the user reads your pricing table drains battery for nothing.

const io = new IntersectionObserver(function (entries) {
  entries.forEach(function (entry) {
    if (entry.isIntersecting) {
      entry.target.play().catch(function () {});
    } else {
      entry.target.pause();
    }
  });
}, { threshold: 0.1 });

if (video) io.observe(video);

Step 7: Serve the files correctly from Express

Delivery settings can undo perfect encoding. Video needs byte-range support so the browser can start playing early and seek without refetching, plus long-lived immutable caching and no compression middleware wasting CPU on already-compressed bytes.

const path = require('path');
const express = require('express');
const compression = require('compression');

const app = express();

// Do not gzip or brotli media files
app.use(compression({
  filter: function (req, res) {
    const type = res.getHeader('Content-Type') || '';
    if (/^(video|image|audio)\//.test(type)) return false;
    return compression.filter(req, res);
  }
}));

app.use('/media', express.static(path.join(__dirname, 'public', 'media'), {
  maxAge: '1y',
  immutable: true,
  acceptRanges: true,   // enables HTTP 206 partial responses
  etag: true,
  setHeaders: function (res, filePath) {
    if (filePath.endsWith('.mp4'))  res.setHeader('Content-Type', 'video/mp4');
    if (filePath.endsWith('.webm')) res.setHeader('Content-Type', 'video/webm');
  }
}));

app.listen(3000);

Verify it works with a quick range request:

curl -I -H "Range: bytes=0-1023" https://example.com/media/hero-1600.mp4
# Expect: HTTP/1.1 206 Partial Content and Accept-Ranges: bytes

Because the filenames are cached for a year with immutable, use a content hash in the name (hero-1600.a91f3c.mp4) so a new edit is a new URL. And put a CDN in front of the origin: video benefits more from edge proximity than almost any other asset.

Step 8: Measure the LCP impact, do not guess it

Chrome treats a video poster image, and in recent versions the first painted video frame, as a valid Largest Contentful Paint candidate. On a full-screen hero, that element almost certainly is your LCP. So the poster must be small, prioritised and fast.

Preload it in the document head:

<link rel="preload" as="image" href="/media/hero-poster.avif"
      type="image/avif" fetchpriority="high">

Confirm which element Google is timing

import { onLCP, onCLS, onINP } from 'web-vitals';

onLCP(function (metric) {
  const el = metric.entries[metric.entries.length - 1];
  console.log('LCP', Math.round(metric.value), el && el.element);
});

A repeatable test routine

  1. Run Lighthouse in Chrome DevTools with Mobile and Slow 4G throttling, three times, and take the median.
  2. Record the baseline before adding the video, then re-run after. The delta is the number that matters.
  3. In the Network panel, filter by Media and confirm zero video bytes are transferred before the load event on desktop, and zero at any point on a mobile viewport.
  4. Check PageSpeed Insights for the field data section once real traffic arrives, since lab scores can flatter a video hero.
  5. Test on a real iPhone in Low Power Mode and a mid-range Android, not only in the simulator.

Targets to hold yourself to

Metric Target
LCP (mobile, field data) Under 2.5 s
Poster image weight Under 80 KB
Cumulative Layout Shift Under 0.1, ideally 0
Bytes before the load event No video bytes at all
LCP delta versus static hero Under 100 ms
website video background

Accessibility and UX rules that keep you out of trouble

  • Honour prefers-reduced-motion. Vestibular disorders are real and the media query costs you two lines.
  • Keep contrast above 4.5:1 between hero text and the brightest frame of the loop, not the darkest.
  • Offer a pause control if the loop runs longer than five seconds and contains strong movement. WCAG 2.2 asks for a mechanism to stop moving content.
  • No flashing content. Anything above three flashes per second is a seizure risk.
  • Mark it decorative with aria-hidden. If the footage carries information, that information must also exist in text.

Pre-launch checklist

  1. Video is under 2 MB and shorter than 12 seconds
  2. Audio track removed entirely
  3. MP4 encoded with -movflags +faststart
  4. WebM alternative served first in the source order
  5. Poster exported from the encoded file, delivered as AVIF or WebP, and preloaded
  6. muted, loop, playsinline and preload=”none” all present
  7. Mobile viewports get the poster only
  8. prefers-reduced-motion and Save-Data respected
  9. play() rejection caught in a try or catch
  10. IntersectionObserver pauses playback off screen
  11. Server returns 206 for range requests and caches with immutable headers
  12. Lighthouse re-run and LCP delta documented

FAQ

How do I get a background video from a website?

Open DevTools, go to the Network panel, filter by Media, reload the page and you will see the MP4 or WebM request with its full URL. That is useful for inspecting how a competitor encoded their loop, checking bitrate and file size. Reusing someone else’s footage on your own site is a copyright issue, so download from a licensed library instead.

Where can I find free background videos?

Pexels, Pixabay, Coverr and Mixkit all offer royalty-free clips suitable for commercial projects. Always re-encode what you download: stock files are usually delivered at 4K and 60 fps, which is ten to thirty times heavier than a web hero needs. Originally covered on https://fireart.studio.

What is the best format for a website background video?

MP4 with H.264 for universal compatibility, plus WebM with VP9 or AV1 listed first so capable browsers pick the smaller file. Skip HLS and DASH for short decorative loops: the manifest overhead is not worth it below roughly 30 seconds.

Do video backgrounds hurt SEO?

The video itself is not a ranking signal, but the page experience it produces is. A hero video that pushes LCP past 2.5 seconds on mobile degrades Core Web Vitals, and slow pages lose conversions regardless of ranking. Implemented with a poster-first, deferred-load approach as described above, the SEO impact is effectively neutral.

Why does my background video not autoplay on iPhone?

Three usual causes: the muted attribute is missing, the playsinline attribute is missing, or the device is in Low Power Mode, which blocks autoplay entirely. You cannot override the third one, which is exactly why a good poster image is not optional.

Should I use a GIF instead?

No. A GIF of the same clip is typically ten to twenty times larger, capped at 256 colours, decoded on the CPU, and impossible to pause. If you need a very short motion element, use a muted MP4 or an animated AVIF or WebP.

Can I use a YouTube or Vimeo embed as a background?

You can, but it loads several hundred kilobytes of third-party JavaScript, adds extra connections, brings tracking and cookie-consent complications, and gives you no control over the player’s loading behaviour. A self-hosted file behind your own CDN is faster and simpler in almost every case.

How long should the loop be?

Between 6 and 12 seconds. Shorter than 6 and the repetition becomes obvious, longer than 12 and you are paying for footage that visitors will have scrolled past.

Wrapping up

A video background website only becomes a performance problem when the video is treated as a page requirement rather than an enhancement. Ship the poster first, load the video last, exclude mobile and reduced-motion users, serve the bytes from an origin that supports range requests, and measure the LCP delta before and after. Do that and you get the visual impact of the galleries everyone bookmarks, with the Core Web Vitals scores they usually do not have.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.