preload='none' Was Correct. Until We Fixed the Server.
When we first wired up the video player for the client training portal, we set preload="metadata" on the <video> element. Reasonable choice. Metadata means the browser fetches just enough to know the duration, build the scrubber, and show a timestamp. The full file stays undownloaded until someone presses play.
That's the theory. The server had different plans.
What preload="metadata" Actually Does
The browser reads video metadata from the MP4's moov atom, which (after running through ffmpeg -movflags +faststart) sits at the front of the file. To get it, the browser makes a partial HTTP request: a Range request for roughly the first few hundred kilobytes.
If the server responds with a proper 206 and the right bytes, the browser reads the moov atom, knows the duration, draws the scrubber, and waits. A 50MB file results in maybe 200KB of network traffic on page load. That's the good outcome.
If the server ignores the Range header and returns a 200 with the full body, the browser reads whatever it gets. It ends up consuming the entire file to locate the metadata. Every page load pulls 50MB regardless of whether the viewer hits play.
Our initial proxy route ignored Range entirely. It called get() on the private blob store, got a stream, returned a 200. No Content-Range, no Accept-Ranges, no partial content. preload="metadata" was silently behaving like preload="auto".
The Workaround
Once we understood this, the right short-term fix was obvious: switch to preload="none" and add a poster image. Nothing downloads until play. The page paints fast. The poster gives the viewer something to look at.
This was correct for the situation we were in. But it came with a cost: no duration display, no scrubber until buffering begins, and on the first seek after pressing play, the browser has to download forward linearly because it has no Range support to jump to.
The portal was usable. It was not great.
Fixing the Server
The Range fix itself is described in the previous post. The short version: Vercel's get() accepts arbitrary request headers. Forwarding the browser's Range header to get() causes the blob store to respond with the actual partial content response, including the real 206 status, Content-Range, and Content-Length. The route relays those upstream headers directly rather than synthesizing its own.
Once that was in place, preload="metadata" worked as documented. The browser sends a Range request, the server honors it, the browser gets the moov atom, the scrubber shows duration, and the rest of the file sits on the CDN until it's needed.
We changed preload back to "metadata" the same day we fixed Range.
The Pattern
The sequence here is easy to fall into. You set a reasonable attribute value, something breaks at a lower level, you change the attribute to work around the broken layer, and the attribute gets the blame. The attribute is not wrong. preload="metadata" is the correct choice for any video long enough that you don't want to download the whole thing on page load.
The server has to be ready for it. If your video delivery goes through any kind of proxy, check that Range requests pass through the full chain. Check for 206 responses, not just 200. Check that Content-Length is set. And then put preload="metadata" back.
The attribute was right the first time.