HLS Streaming vs Progressive MP4: Choosing a Delivery Format

This comparison belongs to Video Performance Optimization within Image & Media Optimization. There are two fundamentally different ways to deliver video on the web. Progressive download serves a single file (MP4 or WebM) that the browser fetches with range requests and plays natively in <video>. Adaptive streaming (HLS or DASH) splits the video into short segments at several bitrates; a player picks segments based on measured bandwidth, switching quality as conditions change.

Each has a performance profile. Progressive files start quickly, need no player JavaScript, and cache trivially — but every viewer gets the same bitrate, so slow connections buffer and fast ones are underserved. Adaptive streaming handles variable networks gracefully and avoids downloading far more than the viewer watches, but requires a manifest round trip before playback and, outside Safari, a JavaScript player (hls.js, Shaka, dash.js) of tens to hundreds of kilobytes that runs on the main thread.

Progressive MP4 vs HLS Comparison of progressive MP4 delivery and HLS adaptive streaming by startup, player cost, network adaptation and caching. Progressive MP4 vs HLS Progressive MP4 • One file, native playback • No player JS • Fixed bitrate for everyone • Best for clips under ~1 minute HLS adaptive streaming • Manifest + segments at several bitrates • Player JS outside Safari (hls.js) • Adapts to bandwidth, less rebuffering • Best for long-form and live content

Rapid Diagnosis

  • Classify your video by length and audience network. Short loops and clips under a minute rarely need adaptive streaming; long tutorials, courses and live streams usually do.
  • Measure startup. Time from play intent to the first frame (playing event); HLS adds manifest and initial segment fetches.
  • Measure player cost. In a trace, look for the streaming library's script evaluation and long tasks around player initialisation.
  • Measure rebuffering. Count waiting events during playback in RUM, segmented by connection type.

Root Cause Analysis

1. Adaptive streaming for short clips. Loading hls.js and manifests for a 15-second loop adds JavaScript and round trips for no benefit.

2. Progressive files for long content on mobile. A single 1080p file stalls on weaker connections and wastes bytes when viewers stop early.

3. Player initialised at load. Streaming players set up on page load, even for videos below the fold or never played, adding main-thread work.

4. Segment caching misconfigured. Manifests cached too long (live) or segments not cached at all (VOD) degrade both freshness and performance.

Time to first frame on a 4G connection (2-minute video) Bar chart comparing time to first frame for progressive MP4 and HLS with native and JavaScript players. Time to first frame on a 4G connection (2-minute video) Progressive MP4 (faststart) 620ms HLS native (Safari) 780ms HLS via hls.js (cold) 1240ms HLS via hls.js (preloaded lib) 860ms

Step-by-Step: Choosing and Implementing

1. Use progressive files for short content

For loops, product clips and short explainers, serve modern-codec progressive files with an H.264 fallback, faststart MP4s and a good poster. No player library is needed.

2. Use HLS for long-form and variable networks, loaded on intent

javascript
const video = document.querySelector('#course-video');
video.addEventListener('click', async () => {
  if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = '/stream/lesson-12/master.m3u8';          // native HLS (Safari, iOS)
  } else {
    const { default: Hls } = await import('hls.js/dist/hls.light.mjs');   // load player only on intent
    const hls = new Hls({ capLevelToPlayerSize: true, startLevel: -1 });
    hls.loadSource('/stream/lesson-12/master.m3u8');
    hls.attachMedia(video);
  }
  video.play();
}, { once: true });
// trade-off: loading the player on click adds its download to time-to-first-
// frame. Prefetch the library on hover or when the video enters the viewport.

Expected outcome: no streaming JavaScript on pages where users never play the video.

3. Cap renditions to the player size

capLevelToPlayerSize (hls.js) or equivalent prevents downloading 1080p segments into a 360px player, saving bandwidth and decode work on mobile.

4. Cache segments immutably and manifests briefly

nginx
location ~* \.(m4s|ts|mp4)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location ~* \.m3u8$       { add_header Cache-Control "public, max-age=2"; }   # live; longer for VOD
# trade-off: for live streams, manifest TTL bounds latency; for VOD, manifests
# never change and can be cached long.

Progressive or adaptive? Decision sequence for choosing progressive download or adaptive streaming for a video. Progressive or adaptive? Live stream? HLS/DASH (adaptive streaming required) yes no Longer than ~1-2 minutes? HLS with the player loaded on intent yes no Audience on highly variable mobile networks? HLS even for medium-length clips yes no Progressive MP4/WebM with a good poster

Verification

Measure time to first frame, rebuffering events and bytes downloaded per view for each format on representative content and networks. Confirm the streaming player loads only when needed (no player requests in the Network panel on page load). In RUM, track rebuffer ratio and average bitrate by connection type to confirm adaptive streaming is adapting.

Worked Example: An Online Course Platform

A course platform served 20–40 minute lessons as single 1080p MP4 files. Mobile learners on weaker connections experienced frequent stalls, and data usage complaints were common because learners often stopped after a few minutes while the browser had buffered far ahead. Switching lessons to HLS with five renditions, loading hls.js only when the play button was pressed (prefetched on hover), and capping levels to player size reduced rebuffering events by 70%, cut average bytes per session by 45%, and left LCP unaffected because the page's LCP was the lesson thumbnail.

The Hidden Cost of Player Libraries

Streaming players do real work on the main thread: parsing manifests, managing buffers, running adaptation logic and handling events. On low-end devices this can produce long tasks during startup and occasionally during playback. Choose lightweight builds (hls.js offers a "light" build without alternate audio and subtitle features), initialise players only when playback is requested, and avoid wrapping them in heavy UI frameworks for controls. Native HLS in Safari has no JavaScript cost, and Chromium is gaining native HLS support in some contexts; feature-detect and prefer native playback where available.

Common Mistakes

  • Streaming tiny clips. Adds latency and JavaScript for no benefit.
  • Initialising players at page load. Costs every visitor main-thread time.
  • No poster for streamed video. Players take longer to show the first frame; a poster covers the gap.
  • Non-faststart MP4s. Progressive playback cannot start until the moov atom arrives.

Edge Cases

DRM. Protected content requires EME and adaptive streaming; budget for license requests in startup time.

Low-latency live. LL-HLS uses partial segments and blocking playlist reloads; CDN support and short manifest TTLs are essential.

Offline downloads. Progressive files are easier to download for offline viewing; HLS requires packaging support.

Analytics. Player analytics plugins add more JavaScript; load them with the player, not at page load.

FAQ

Is DASH better than HLS?

They are functionally similar. HLS plays natively in Safari and iOS, which makes it the pragmatic default; DASH requires a JavaScript player everywhere. Many platforms package both from the same CMAF segments.

Does adaptive streaming improve Core Web Vitals?

Not directly — playback quality is outside Core Web Vitals. Done carelessly (player loaded at startup) it can hurt INP and LCP; done well (loaded on intent) it is neutral for vitals and much better for playback.

Can progressive files adapt to the device?

Partially: <source media> can pick a rendition by viewport size, and multiple codecs by type. They cannot adapt to bandwidth during playback.

What segment length should I use?

Two to six seconds is typical. Shorter segments adapt faster and start sooner but add request overhead; longer segments compress slightly better.

Should video segments go through the main site CDN?

Yes, ideally on the same or a well-connected CDN with immutable caching and range-request support. Separate video hostnames add connection setup, which a preconnect on play intent can hide.

How do I measure rebuffering?

Listen for waiting and playing events, sum the time between them during playback, and divide by watch time to get a rebuffer ratio. Report it per format and connection type.

Can I start with progressive files and add HLS later?

Yes. Keep the poster and container markup independent of the delivery format, and wrap source selection in one component. Switching a template from progressive to HLS then changes only that component, and you can compare startup and rebuffering metrics before and after on the same pages.

/html>