Design YouTube
A video site with a fast watch page, a custom player that adapts quality to the network, a mini-player that keeps playing across pages, a home grid and recommendations, comments, and resumable uploads. The hard parts are a quick first frame, playback without stalls on any network, one player that outlives every page, and gigabyte uploads that survive drops.
The question
Design the web client for YouTube. Someone opens a shared link to a video: it should start playing fast, even on a weak phone on a train, and they can browse recommendations and read comments while it plays. If they go back to the home page, the video keeps playing in a small player in the corner.
Creators also upload videos from the browser, often several gigabytes, and fill in the title and thumbnail while the video processes. Walk me through how you'd build the client, and be ready to talk about live streams, ads, and downloads at the end.
Real-life examples: YouTube watch page and mini-player, YouTube Studio uploads, Vimeo, Twitch VODs, Dailymotion
Requirements: what to ask before drawing anything
"Design YouTube" is too big for 45 minutes. It's a home page, a watch page, a video player, a recommendation rail, comments, an upload studio, live streams, ads, and downloads. Each could fill an interview by itself. The first five minutes are for agreeing which parts to design in depth, and the way to do that is to ask questions whose answers change the design.
Start with the surface (which screens), then the users (devices and networks), then the content (video length, upload size), then the rules (search engines, autoplay, rights). The table shows the answers this guide assumes, and what moves if the interviewer answers differently.
| Question | Answer we assume | What it changes |
|---|---|---|
| Which screens? | Home grid, watch page, mini-player, comments, and the upload flow. Live, ads, and downloads are follow-ups. | Which chapters get depth. Each extra surface is its own design. |
| Which devices? | Desktop and mobile web. Assume about half of watch time is on phones, many 4 to 6 years old with 2 to 4 GB of memory. | JavaScript budget, which codecs to request, and how much to buffer. |
| Which networks? | Fiber at 300 Mbps down to 1 Mbps on a train. Bandwidth can fall by 80 percent in one second. | Adaptive streaming and the rules that pick quality. |
| Search engines and link previews? | Yes. Many views start from search results or links shared in chat apps. | The watch page must be server-rendered with real metadata. |
| How long are videos? | From 15 seconds to 10 hours. Assume a median of about 10 minutes. | Buffer limits, seeking, and the size of preview frames for the seek bar. |
| Does playback continue while browsing? | Yes. Leaving the watch page shrinks the video into a mini-player. | Who owns the player: the page or the app shell. |
| How big are uploads? | YouTube accepts up to 256 GB. We design for 2 to 20 GB from a laptop on home Wi-Fi. | Resumable, chunked uploads that survive drops and reloads. |
| How busy are comments? | Popular videos have hundreds of thousands. Most viewers never scroll to them. | Lazy loading and cursor pagination. |
Functional requirements
- Open
/watch?v=IDand start playing, at&t=95swhen the link has a start time. - Pick video quality automatically, with a manual choice in a settings menu, plus captions and playback speed.
- Control the player with mouse, touch, and keyboard: play, pause, seek with preview frames, volume, fullscreen, theater mode.
- Show a recommendations rail beside the video, with the first card as Up next, and autoplay it when the video ends.
- Keep playing in a mini-player while the user browses other pages, and expand back without a restart.
- Show a home grid of video cards with thumbnails and a short preview on hover.
- Load comments, sort them by Top or Newest, expand replies, and post a comment or reply.
- Upload a video, see its progress and processing status, and edit the title, description, and thumbnail while it processes.
Non-functional requirements, with numbers
For a video site, the classic web numbers are not enough. Core Web Vitals measure the page; viewers judge the video. So we track both families, at the 75th percentile of real plays, split by device class and network.
- Time to first frame (from navigation to the first decoded frame on screen) under 1 s on broadband and under 2 s on a mid-range phone on 4G.
- Rebuffer ratio (time spent stalled divided by watch time) under 0.5 percent. A stall is the spinner in the middle of playback.
- Start failures: fewer than 1 in 500 plays never start.
- Page vitals: LCP (Largest Contentful Paint) under 2.5 s, INP (Interaction to Next Paint) under 200 ms, CLS (Cumulative Layout Shift) under 0.1.
- Data: never buffer more than about 60 s ahead, never fetch a quality taller than the player on screen needs, and respect the user's data saver setting.
- Low-end devices: the code needed to start playback stays under about 150 KB compressed, and a 2019 budget phone plays 480p without dropped frames.
- Accessible: every control works from the keyboard, the player makes sense to screen readers, and captions are available on every video.
- Uploads: a dropped connection or a page reload loses at most one chunk of work, never the whole file.
Out of scope, said out loud
Name what you won't design. It shows you know where the edges are, and it gives the interviewer a chance to steer you before you spend ten minutes in the wrong place.
- Transcoding and storage. The servers turn an upload into many encoded copies; the client only uploads and then watches the status.
- The recommendation model. The client shows the list it's given and reports what was seen and watched.
- Shorts, the vertical swipe feed. It's a different player lifecycle with aggressive preloading, and it's its own problem.
- DRM for paid movies. We mention it with downloads, but regular videos are not encrypted.
Check your understanding · Exercise 1
Which answers from the interviewer would change this design the most? Select all that apply.
What makes a video site hard
Before picking anything, name the forces. Every decision in this guide comes from one of the properties below. If you can list them, you can rebuild most of the design live, instead of reciting it.
- The bytes are huge and never stop. A 10-minute video at 1080p is about 375 MB. The page's own JSON and HTML are about 100 KB. Video is over 99 percent of the bytes, and it keeps downloading the whole time the user watches.
- The network changes during playback. A phone moves from Wi-Fi to 4G, enters a tunnel, or shares a link with a sibling's game download. Whatever quality was right 30 seconds ago may be wrong now.
- Viewers judge the first seconds. A study of 23 million views by Krishnan and Sitaraman (2012) found viewers start leaving when a video takes more than 2 seconds to start, and each extra second loses about 6 percent more of them.
- A stall is worse than blur. Viewers forgive a softer picture far more than a spinner. So the goal is not the highest quality, it's the highest quality that never runs out.
- The player outlives the page. Playback continues while the user goes home, searches, or opens a channel. In a single-page app, pages mount and unmount all the time; the player must not.
- Most of the page is never seen. Comments and the lower rail are below the fold. Many viewers never scroll there, so everything they cost must be paid only when someone looks.
- Devices differ in what they can decode. A new laptop decodes AV1 4K in hardware. A budget phone decodes AV1 in software, drops frames, and drains its battery. The right file depends on the chip, not only the network.
- Creators push gigabytes up. A 5 GB upload over a 20 Mbps home uplink takes about 35 minutes. Over that time, connections drop and laptops sleep.
Put numbers on the bytes, because they set every budget later. Video quality is measured by bitrate: how many bits per second of video the file holds. Megabits per second (Mbps) divided by 8 gives megabytes per second, so 1 Mbps is about 7.5 MB per minute. The table shows a typical bitrate ladder: the list of encoded copies, called renditions, that the server makes from one upload.
| Rendition | Typical bitrate | Data per minute | One 4 s segment |
|---|---|---|---|
| 240p | 0.4 Mbps | 3 MB | 200 KB |
| 360p | 0.8 Mbps | 6 MB | 400 KB |
| 480p | 1.4 Mbps | 10.5 MB | 700 KB |
| 720p | 2.8 Mbps | 21 MB | 1.4 MB |
| 1080p | 5 Mbps | 37.5 MB | 2.5 MB |
| 2160p (4K) | 16 Mbps | 120 MB | 8 MB |
Two consequences follow. First, a single thumbnail (15 KB) is tiny next to one segment of video, but twenty thumbnails plus fonts plus scripts can still delay the first segment on a 1 Mbps link. Video must get the network first. Second, a viewer who leaves after 20 seconds should not have downloaded 2 minutes of 1080p: that's 75 MB thrown away, and on a metered plan it costs them money.
Think first
A teammate asks: "Why not just put <video src="movie.mp4"> on the page? Browsers can play MP4." What goes wrong, and for whom?
It plays, and for a short clip on fast Wi-Fi it's fine. But one file means one quality for the whole video. Choose 1080p and the phone on a train stalls every few seconds; choose 360p and the 4K monitor looks blurry for everyone.
It also gives you no control over how much is downloaded ahead (the browser decides), no way to switch quality when the network changes, and no clean way to measure why playback stalled. The fix is adaptive streaming: many renditions cut into short segments, with the player choosing each segment's quality. The next chapters build that.
The watch page: what loads first, and how it renders
The watch page has one job in its first second: show a frame of video. Everything else on it is ranked against that goal. In order, the viewer needs the player box with a poster image, then the first video frame, then the title and channel, then the like and subscribe buttons, then the rail, then the description, and last the comments. If the rail or the comments slow the first frame by even 200 ms, the order is wrong.
The naive version is a client-rendered single-page app. The server sends an empty HTML shell and a 1 MB JavaScript bundle. The bundle runs, asks the API for the video's data, then asks the CDN for the manifest (the file listing the video's renditions), then for the first segment.
On a mid-range phone on 4G, with a round trip of 150 ms, that chain looks like this: HTML 300 ms, JavaScript download and parse 1,800 ms, API 300 ms, manifest 200 ms, first segment 500 ms. That's about 3 seconds of blank page before the first frame, and a crawler or a chat app's link preview sees an empty page.
Decision How should the watch page render on first load and when the user moves between videos?
What we pick: Streaming server rendering for the first load, client-side navigation after it. It's the only option that gives a fast first frame, real metadata for search and link previews, and a page that never reloads, which the mini-player needs. The cost is two rendering paths, which a framework with server components or streaming SSR already handles.
What the first response contains
The server's job is to cut the waterfall: everything the player needs to fetch its first segment must be in the first bytes of HTML, so the browser starts the video download before the app's JavaScript has even parsed. In practice that means five things in the head and the top of the body.
<link rel="preconnect">to the video CDN's host, so the DNS lookup and TLS handshake (100 to 300 ms on mobile) happen while the HTML is still arriving.- The poster image, a plain
<img>withfetchpriority="high"inside a box withaspect-ratio: 16 / 9. It's usually the LCP element, and because it's in the HTML, the browser finds it at once. - The player data inlined as JSON in a script tag: the manifest URL, the first segment URLs for a middle quality, caption tracks, and the preview-frame sprite. Inlining saves a whole API round trip.
- A small player bootstrap script, separate from the rest of the app. A
modulepreloadlink in the head fetches it early with high priority, and anasyncmodule script placed right after the player data runs it as soon as it arrives, without waiting for the rest of the streamed HTML. It reads the inline JSON, creates the video pipeline, and requests the first segment, even before the rest of the page hydrates. - Metadata for machines: the
<title>, Open Graph tags (og:title,og:image,og:video) for link previews, andVideoObjectstructured data in JSON-LD so search engines can show the video with its duration and thumbnail.
<head>
<title>How CDNs work - Tech Explained - YouTube</title>
<link rel="preconnect" href="https://v1.videocdn.example" crossorigin>
<meta property="og:title" content="How CDNs work">
<meta property="og:image" content="https://i.example/vi/abc123/hq.jpg">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"VideoObject","name":"How CDNs work","thumbnailUrl":"https://i.example/vi/abc123/hq.jpg","uploadDate":"2025-03-14","duration":"PT12M5S"}</script>
<link rel="modulepreload" href="/player-boot.js" fetchpriority="high">
</head>
<body>
<div class="player-slot">
<img src="https://i.example/vi/abc123/hq.webp" fetchpriority="high" alt="">
</div>
<script id="player-data" type="application/json">
{"videoId":"abc123","manifest":"/m/abc123.mpd?sig=...","start":95}
</script>
<script type="module" async src="/player-boot.js"></script>
<h1>How CDNs work</h1>
<!-- the rail streams in later -->
Then the server streams. The head, the player box, and the title leave as soon as the video's own record is read, often within 50 ms. The rail depends on the recommendation service, which may take 300 ms or more, so it's sent later in the same response.
A slow recommendation call now delays only the rail, never the player. Comments aren't server-rendered at all; we'll see why in the comments chapter.
Read the start time on the server too. For /watch?v=abc123&t=95s, the server should pick a poster frame near 95 seconds and put the segment that covers 92 to 96 seconds first in the player data. If only the client reads t, the browser downloads segment 0, then throws it away and seeks: a wasted 700 KB and a visible jump.
Moving between videos without reloading the player
After the first load, the client router takes over. When the user clicks a card in the rail, the router stops the browser's default navigation, calls history.pushState with the new URL, and fetches one JSON document for the next video: its player data, title, channel, description, and the first page of the rail. Then it asks the player to load the new video and swaps the page content around it.
The player is told "load video xyz", not destroyed and recreated. It keeps the same <video> element, so fullscreen, volume, captions language, playback speed, and Picture-in-Picture all carry over. Some mobile browsers also tie permission to play with sound to the element the user already tapped, so reusing it avoids a muted or blocked start on the next video.
Inside, the player tears down the old pipeline, attaches a fresh one, and requests the new video's first segment, often from data that was prefetched when the user hovered the card.
Two details make client navigation feel like a real page change. Move keyboard focus to the new video's <h1> (given tabindex="-1" so it can take focus), so screen reader users hear the new title instead of silence. And set document.title, so the tab, history, and screen readers all agree on where the user is. Back and Forward fire popstate, which the router handles the same way: fetch or reuse the data, load the video, restore the scroll position.
Check your understanding · Exercise 2
Which changes shorten the time to first frame on a first visit to a watch page? Select all that apply.
Adaptive streaming: manifests, segments, and Media Source Extensions
The think-first question in the forces chapter ended with the fix: many renditions, cut into short pieces, with the player choosing each piece. This chapter builds that pipeline. It's the heart of the design, because every number on the board (first frame, rebuffer ratio, data use) is decided here.
Start from the server's output. After an upload, the transcoder makes the bitrate ladder shown earlier and cuts every rendition into segments: short chunks of 2 to 6 seconds each. Every segment starts with a keyframe, a frame that can be decoded without the frames before it.
That's what makes switching possible: segment 7 at 480p can follow segment 6 at 1080p, because neither needs the other to decode. Audio is usually a separate stream (one 128 kbps track per language), so a quality switch only changes the video.
Then a manifest describes it all: which renditions exist, their bitrate, resolution, and codec, and where each segment lives. There are two common formats.
HLS (HTTP Live Streaming, from Apple) uses text playlists ending in .m3u8: a multivariant playlist lists the renditions, and one media playlist per rendition lists its segments. DASH (Dynamic Adaptive Streaming over HTTP, an MPEG standard) uses one XML file, the MPD, with adaptation sets (video, audio, captions) holding representations (the renditions). Both can point at the same CMAF segments (fragmented MP4 files), so a site can store one copy of the media and serve both formats.
#EXTM3U
# multivariant playlist: one line pair per rendition
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.4d401e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
# 720p/index.m3u8: the media playlist for one rendition
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:4
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.0,
seg0.m4s
#EXTINF:4.0,
seg1.m4s
#EXT-X-ENDLIST
Decision How should the browser get video bytes into the video element?
What we pick: A script player on MSE (or ManagedMediaSource on iPhone), with native HLS as the fallback on old iPhones that have neither. For a site whose whole product is playback, the control is worth the code: quality rules, buffer limits, prefetching the next video, and precise stall metrics all need it. Plain MP4 stays useful for hover previews.
How the MSE pipeline works
The player creates a MediaSource, attaches it to the video element through an object URL, and waits for sourceopen. Then it adds one SourceBuffer per track, named by its exact codec string, such as video/mp4; codecs="avc1.640028". Before that, MediaSource.isTypeSupported tells it which codecs this browser can play at all, so it can drop renditions it can't use.
Each track is fed in two kinds of pieces. The init segment is small (about 1 KB) and holds the codec setup; it's appended once per rendition, and again whenever the player switches to another rendition. Media segments follow.
appendBuffer is asynchronous: the buffer is busy until it fires updateend, and appending again before that throws an error. So a real player keeps a small queue per buffer.
const media = new MediaSource();
video.src = URL.createObjectURL(media);
await once(media, "sourceopen");
const videoBuf = media.addSourceBuffer('video/mp4; codecs="avc1.640028"');
async function append(buf: SourceBuffer, url: string, signal: AbortSignal) {
const res = await fetch(url, { signal });
const bytes = await res.arrayBuffer();
if (buf.updating) await once(buf, "updateend");
try {
buf.appendBuffer(bytes);
} catch (err) {
if ((err as DOMException).name !== "QuotaExceededError") throw err;
await evictBehind(buf, video.currentTime - 30); // free memory, then retry
buf.appendBuffer(bytes);
}
await once(buf, "updateend");
}
await append(videoBuf, rendition.init, controller.signal); // codec setup
await append(videoBuf, rendition.segments[index], controller.signal);
once wraps an event in a promise.Memory is the limit people forget. Browsers cap what a SourceBuffer may hold (in Chrome, about 150 MB for video), and phones with 2 GB of memory hit trouble well before that. When the cap is reached, appendBuffer throws QuotaExceededError.
So the player removes what it no longer needs with remove(start, end): keep about 30 seconds behind the playhead for quick rewinds, and drop the rest. With 60 seconds ahead and 30 behind at 1080p, that's about 55 MB held, which fits comfortably.
appendBuffer, and frees memory with remove.Two more pieces finish the pipeline. A scheduler decides when to fetch the next segment: whenever the buffer ahead (the end of buffered minus currentTime) is below the goal, fetch one more; when it's full, wait. And the ABR logic decides which rendition each fetch should use. That decision has its own chapter next, because it's where most of the quality the viewer feels comes from.
Check your understanding · Exercise 3
A viewer on a cheap phone watches a 2-hour lecture. After 40 minutes, playback stops with an error. The player logs QuotaExceededError. What's the right fix? Select all that apply.
Choosing quality: throughput, buffer, and the first segment
ABR (adaptive bitrate) is the rule that picks the rendition for the next segment. The naive rule is one line: measure how fast the last segment downloaded, then pick the highest rung below that speed.
It fails in three ways. At the very start there is no measurement yet. Measurements are noisy: one segment can download at 20 Mbps and the next at 2 Mbps on the same phone. And a rule that follows every sample switches quality every few seconds, which viewers notice as the picture getting sharp and soft over and over.
Here is how it goes wrong in numbers. A viewer on a train has been getting 8 Mbps, so the player picks 1080p at 5 Mbps. The train enters a cutting and the link drops to 1 Mbps.
The next 1080p segment is 2.5 MB, which now takes 20 seconds to download. If the buffer held 12 seconds, the viewer sees a spinner for 8 seconds, all because the rule trusted a number that was true 4 seconds ago.
Decision Which rule should pick the quality of each segment?
What we pick: The hybrid. Startup is exactly where the buffer rule is weakest and the throughput rule is good enough, and steady playback is where the buffer rule is safest. Add the guards, and log every switch with its reason, so the tuning can be checked against real rebuffer ratios.
Pick a network and a rule, then press Next segment.
Measuring throughput without fooling yourself
Each finished segment gives one sample: bytes divided by download time. Three rules keep the estimate honest. First, ignore tiny downloads (under about 16 KB, like init segments): they measure the round trip, not the bandwidth.
Second, keep two moving averages, a fast one that reacts within a couple of seconds and a slow one that changes more gradually, and use the lower of the two; Shaka Player does this (with half-lives of 2 and 5 seconds by default), so drops count at once but spikes don't.
Third, measure while the download is still running: if half a segment has arrived and the rest will finish after the buffer runs out, abort it with the AbortController and fetch the same segment at a lower rung.
Startup against quality
The first segment is a bet with no data. A low first rung starts fast (a 240p segment is 200 KB, under a second on 2 Mbps) and looks soft for a few seconds. A high one looks great on fiber and takes 10 seconds on a slow phone. Since starting fast matters more than the first seconds looking sharp, the bet should lean low, and use every hint available.
- The last session's estimate, saved in
localStoragewith a timestamp. If it's under a day old and on the same kind of connection, start from it. navigator.connection.downlinkandeffectiveType, from the Network Information API. Only Chromium browsers have it, and the value is rounded and capped (at 10 Mbps in Chrome), but it's better than nothing.- The server's guess, placed in the inline player data from what the edge saw during the HTML download.
- A default of 360p or 480p when nothing else is known, then a quick climb once two or three segments have reported.
Start playing as soon as one segment is buffered, not when the buffer goal is reached. Then grow the buffer while playing. After a stall, the trade-off flips: resuming after one segment shortens this stall but risks another right away, and two short stalls feel worse than one. Waiting for about 5 seconds of buffer before resuming is a common middle ground.
Caps that beat any bandwidth rule
Some limits come before bandwidth. Never pick a rendition taller than the player on screen times devicePixelRatio: a 640 by 360 player on a 2x screen needs 720p at most, and the 400 by 225 mini-player needs 480p at most even on a 2x screen. Watch video.getVideoPlaybackQuality(): if more than about 10 percent of frames are dropped at a rung, the device can't decode it smoothly, so cap below it for the session.
Ask navigator.mediaCapabilities.decodingInfo before choosing a codec: it says whether AV1 or VP9 at a given size will be smooth and powerEfficient (in hardware) on this device. On a budget phone, H.264 in hardware beats AV1 in software, even though AV1 needs fewer bits. Finally, honor the user's data saver choice and a manual quality pick: both override everything else.
Check your understanding · Exercise 4
The rebuffer ratio is fine, but viewers on fast home Wi-Fi complain that videos look blurry for the first 20 seconds. Which changes help? Select all that apply.
The player: states, seeking, and controls
With bytes flowing, the player needs a face: the controls, the spinner, the menus. The naive way is a handful of booleans: isPlaying, isLoading, isSeeking, hasError. Each media event flips one.
After a week of bug reports, the screen shows a spinner and a play button at the same time, or a seek during a stall leaves the player thinking it's paused when it's playing. Four booleans allow 16 combinations, and only about 6 of them mean anything.
Decision How should the player track what it's doing?
What we pick: The state machine, plus one extra fact beside it: the viewer's intent (play or pause). Seeking and buffering are temporary; when they end, intent says whether to go to playing or paused. The UI then reads one value: show the spinner in buffering, the big play button in ready, the replay button in ended.
The video element's events drive the transitions. playing means frames are moving. waiting means playback stopped because the next frame isn't buffered: that's a stall. seeking and seeked bracket a jump. ended and error speak for themselves.
One rule makes the spinner feel calm: enter buffering at once (so the stall timer starts for analytics), but only show the spinner after about 300 ms. Most short waits end before then, and a spinner that flashes for 100 ms looks like a bug.
The player is idle. Press Load video to start, then try events in any order.
Seeking and the seek bar
A seek inside the buffered range is instant: set currentTime and the decoder jumps. A seek outside it is a small first load. Abort the segment in flight (it's for the wrong position), find the segment that holds the target time, and fetch it, often one rung lower so the picture returns fast. Keep the old buffered ranges if memory allows: viewers often jump back to where they were.
Dragging the seek bar must not seek on every pixel, or the player fetches dozens of segments the viewer never watches. While dragging, show a preview frame above the pointer and seek once on release.
The preview frames come from storyboards: sprite sheets made at upload, each one JPEG holding a grid of small frames (for example 10 by 10 frames of 160 by 90, one every few seconds). One 60 KB sheet covers several minutes, so the whole bar's previews for a 10-minute video cost about 200 KB, fetched only when the viewer first hovers the bar.
Quality, captions, speed, and shortcuts
- Quality menu: Auto (showing what it's playing now, like Auto 720p) plus each rung the device can play. A manual pick turns off ABR up-switches and is remembered for the session. Hide rungs above the screen's size times
devicePixelRatio. - Captions: WebVTT files, one per language, listed in the player data and fetched only when turned on. Set the track's
modetohidden, so its cues still firecuechangebut the browser doesn't draw them, and render the text yourself. Then captions sit above the controls and follow the viewer's size and color settings. - Speed:
playbackRatefrom 0.25 to 2, withpreservesPitchleft on so voices don't sound like chipmunks. At 2x, the buffer drains twice as fast, so ABR must count buffer in seconds of wall time and expect to need twice the bandwidth. - Fullscreen: call
requestFullscreenon the player container, not the video, so custom controls and captions come along. iPhone Safari only allows fullscreen on the video element itself, with its native controls, so plan for that. - Shortcuts:
kor Space to play and pause,jandlfor 10 seconds back and forward, arrows for 5 seconds and volume,ffullscreen,mmute,ccaptions,0to9to jump to 0 to 90 percent. Ignore them while focus is in a text field, or typing a comment pauses the video.
Check your understanding · Exercise 5
The video is playing. The viewer drags the seek bar far ahead, past the buffer, and the network is slow. What should happen? Select all that apply.
A mini-player that survives navigation
The viewer is 4 minutes into a video and clicks the logo to go home. The video should shrink into a small player in the corner and keep playing without a hiccup: same frame, same audio, no spinner.
In a typical single-page app, that's exactly what breaks. The router unmounts the watch page, the watch page owns the player, and the <video> element goes with it. The buffer, the MSE pipeline, and the decoder are all thrown away.
The fix is a question of ownership. Which part of the app should own the one thing that must live across pages? Three answers come up in interviews.
Decision Where should the player live so that it survives page changes?
What we pick: The shell owns one player, positioned over a slot. It's the only option where the element never leaves the document, which is what makes playback truly continuous. The layout cost is small and well understood: one ResizeObserver and a few lines of positioning.
How the slot and the host talk
The watch page renders the slot with aspect-ratio: 16 / 9 and registers it with the player host through context. The host measures it with getBoundingClientRect, adds the scroll offset, and places the player with position: absolute in page coordinates. Because the player scrolls with the page, scrolling needs no JavaScript.
A ResizeObserver on the slot catches the rest: theater mode, a window resize, the rail collapsing on a narrow screen. When the slot unregisters, the host switches to position: fixed in the bottom corner, about 400 by 225 pixels on desktop, and animates the change with a transform (skipped when prefers-reduced-motion is set).
The mini-player is a small region of its own, labeled "Mini player", with play, close, and expand buttons that work from the keyboard. Expand pushes /watch?v=ID&t=current-seconds to the router; the watch page mounts, registers its slot, and the host moves the player back over it. Close stops playback and frees memory: abort fetches, end the MediaSource, revoke the object URL. A click on another video from any page simply tells the host to load the new video, the same call client navigation uses.
Beyond the tab: Media Session and Picture-in-Picture
Two browser APIs extend the same idea outside the page. The Media Session API puts the title, channel, and artwork on the phone's lock screen and in the desktop media hub, and routes hardware keys (play, pause, next track) to your handlers. And video.requestPictureInPicture() (in Chromium and Safari; Firefox offers its own built-in button instead) floats the video in a window above other apps, which is the mini-player for viewers who switch tabs. Chromium's Document Picture-in-Picture goes further and lets that window hold your own controls.
navigator.mediaSession.metadata = new MediaMetadata({
title: video.title,
artist: video.channelName,
artwork: [{ src: video.thumb96, sizes: "96x96", type: "image/jpeg" }],
});
navigator.mediaSession.setActionHandler("play", () => player.play());
navigator.mediaSession.setActionHandler("pause", () => player.pause());
navigator.mediaSession.setActionHandler("seekforward", () => player.seekBy(10));
navigator.mediaSession.setActionHandler("nexttrack", () => player.load(upNext.id));
Check your understanding · Exercise 6
The player lives in the app shell. Which of these still need care? Select all that apply.
The home grid, recommendations, and prefetching the next video
The home page is a grid of video cards: thumbnail, duration badge, title clamped to two lines, channel, views, and age. It looks simple, and on a phone it's the page where most sessions begin, so its cost is felt by everyone. The data comes from a recommendations endpoint in pages of about 24 cards, with a cursor for the next page as the viewer scrolls.
Thumbnails: most of the page's bytes
A screen of 12 cards is 12 images, and the grid can grow to hundreds. Each thumbnail is served in several widths (say 168, 336, and 720 pixels) as WebP or AVIF, and the browser picks with srcset and sizes. A 336-pixel WebP is about 15 to 20 KB.
Only the first row is loaded eagerly; the rest use loading="lazy", and only the first visible image gets fetchpriority="high", because it's the likely LCP element. Every card's image box has aspect-ratio: 16 / 9 and a background of the thumbnail's average color (sent with the card data), so nothing shifts when images arrive and the page looks filled in before they do.
After an hour of scrolling, the grid can hold 600 cards. Rendering them all keeps the DOM large and makes every style change slow. Two tools fix that.
content-visibility: auto on each row lets the browser skip layout and paint for rows far off screen, with contain-intrinsic-size holding their height. For very long sessions, windowing (rendering only the rows near the viewport) caps the DOM outright; it costs more code and care with focus, so start with content-visibility and measure.
Hover previews
Resting the pointer on a card plays a short muted preview. Done carelessly, a mouse swept across the grid starts eight videos. The rules: start only after the pointer rests for about 500 ms; play one preview at a time; use a small dedicated file (a few seconds at the lowest rung, about 300 KB) in a separate lightweight <video>, never the main player; stop and release it on pointerleave.
Offer previews only where (hover: hover) matches, and skip them when the viewer has asked for less motion or less data. On touch screens, a muted preview for the card centered in the viewport is a product choice, and it should be off on metered connections.
Decision What should the client fetch before the viewer asks for it?
What we pick: Prefetch by intent. The watch JSON is small, so fetching it on hover costs little even when wasted. Media is large, so it's prefetched only for the one video most likely next: Up next, near the end of the current one. Every prefetch is skipped when Save-Data is on, when the connection is slow, or when the current video's buffer is under about 30 seconds.
The rail and Up next
On the watch page, the rail is the home grid's narrow cousin: the first 10 to 20 cards stream in with the server render, and more arrive by cursor as the viewer scrolls. The first card is Up next.
When the video ends with autoplay on, the player shows a countdown of a few seconds with a Cancel button, announces it politely to screen readers, and then loads Up next through the same call as a click. Because its first segment was prefetched, the next video starts in about 100 ms.
The client router shapes what's worth prefetching. The Speculation Rules API can prerender whole pages, which suits multi-page sites; here the shell never reloads, so the useful thing to prefetch is data and media, not documents. Use fetch with priority: "low", where supported, so the browser schedules it behind the playing video's segments.
Check your understanding · Exercise 7
Which prefetching rules are sound for this design? Select all that apply.
Uploads: resumable chunks, processing states, and editing meanwhile
A creator picks a 5 GB file. Their home uplink is 20 Mbps, so the upload takes about 35 minutes. The naive form posts the whole file in one request. At minute 30, the Wi-Fi blinks, the laptop sleeps, or someone closes the tab: the request fails and 4 GB of progress is gone.
The creator starts over, and it may fail again. Big uploads need a protocol where the unit of failure is small.
Decision How should the browser send a multi-gigabyte file?
What we pick: A resumable session, sent in chunks, one or two at a time. Home uplinks are the bottleneck, so parallelism buys little, while resumability saves whole uploads. The design keeps the server as the source of truth for the offset, so a client that crashed, reloaded, or changed networks can always ask and continue.
bytes */total and continues from the byte after the returned range, even if that is mid-chunk.The client side of a resumable upload
POST /uploadswith the file's name, size, and type. The reply holds the session URL and a draft video ID, so editing can start at once.- Save the session in IndexedDB, keyed by the file's name, size, and
lastModified. - Cut chunks with
file.slice(start, end), which reads from disk lazily and never loads the 5 GB into memory. Keep chunk sizes a multiple of 256 KiB, as Google's protocol requires. - Send each chunk with
Content-Range: bytes 0-8388607/5368709120. The server answers 308 with aRangeheader saying what it has; the last chunk gets 200 or 201 with the video record. - On failure, wait with exponential backoff and jitter (1, 2, 4 seconds, up to a minute), and retry at once when the
onlineevent fires. Before resending, send an emptyPUTwithContent-Range: bytes */5368709120to ask the offset, and resume from the byte after it.
Two browser facts shape the rest. First, fetch has no upload progress events, so the chunk sender uses XMLHttpRequest and its upload.onprogress to draw a smooth bar.
Second, after a reload the page can't reopen the file by itself. It finds the saved session and asks the creator to pick the same file again, checks name, size, and modified time, and continues from the server's offset. Chromium's File System Access API can store a file handle in IndexedDB and skip that step after a permission prompt.
The upload manager lives in the app shell, like the player, so the creator can move around the studio while it runs. While bytes are in flight, a beforeunload handler asks before the tab closes. That's one of the few honest uses of that prompt.
Processing, and editing while it happens
When the last byte lands, the video isn't watchable yet. The servers transcode it: a low-quality version first (minutes), then HD and 4K (much longer), plus storyboards and automatic captions, and a copyright check runs in parallel.
The client shows these as named states, driven by the server: uploading, processing, sd-ready, hd-ready, and finally ready, or failed with a reason. It polls a status endpoint every 5 to 10 seconds, slower when the tab is hidden, or listens on a server-sent events stream if the backend has one.
Meanwhile the creator fills in the title, description, tags, and thumbnail. Because the draft video ID came back with the session, the form saves to it from the first keystroke: a PATCH /videos/ID debounced by about a second, with a version number so two open tabs can't silently overwrite each other. Auto-generated thumbnail choices appear when processing produces them. The creator may press Publish before HD is ready; the video goes live at SD and gains higher rungs as they finish.
Check your understanding · Exercise 9
A creator's upload fails at 3.1 GB of 5 GB when their laptop sleeps. They wake it 20 minutes later on a different Wi-Fi network. What should the client do? Select all that apply.
Architecture, data model, and API
Now the pieces from the earlier chapters can be put on one board. The rule that drew every boundary: things that must outlive a page (the player, uploads, analytics) belong to the shell; things that belong to one page (metadata, rail, comments) live in that page and can be thrown away.
| Part | Owns | Lives |
|---|---|---|
| App shell and router | Layout, navigation, focus and title on route change | For the whole session |
| Player host | The one video element, MSE pipeline, ABR, state machine, slot or mini mode | For the whole session |
| Data cache | Watch data by video ID, rail and home pages by cursor, comment pages by video and sort | Entries expire after minutes |
| Watch page | Title, actions, description, slot, rail, the comments sentinel | While on the page |
| Upload manager | Sessions, chunk queue, retries, progress | For the whole session, backed by IndexedDB |
| Analytics client | Event buffer, heartbeats, beacons on page hide | For the whole session |
Video bytes never touch the API servers. The API returns JSON with signed URLs, and the player fetches the manifest and segments straight from the CDN, which is built to serve huge files from close to the viewer. The edge render server is a separate box because it does a different job: it turns the first request into HTML fast, close to the viewer, and streams.
Client data model
type VideoCard = {
id: string;
title: string;
channel: { id: string; name: string; avatar: string };
durationSec: number;
views: number;
publishedAt: string; // ISO date; format with Intl on the client
thumb: { base: string; widths: number[]; color: string };
};
type PlayerData = {
manifestUrl: string; // signed, expires
expiresAt: string;
startSec: number;
captions: { lang: string; label: string; url: string }[];
storyboard: { url: string; cols: number; rows: number; everySec: number };
};
type WatchData = { video: VideoCard & { description: string; likes: number; commentCount: number }; player: PlayerData; rail: Page<VideoCard> };
type Page<T> = { items: T[]; next: string | null }; // next is an opaque cursor
type PlayerSnapshot = { videoId: string | null; state: PlayerState; intent: "play" | "pause"; rung: number; mode: "slot" | "mini" };
PlayerState is the union from the player chapter.Where each piece lives matters as much as its shape. Server data (watch data, pages of cards and comments) sits in a query cache keyed by video ID, sort, and cursor, so going Back shows the previous video's page at once.
The player's snapshot lives in a small store outside React, read through useSyncExternalStore; the playhead time is not in it, because it changes many times a second, and the seek bar reads it straight from the element in an animation frame. The URL holds only what a shared link needs: v and t.
Preferences (volume, caption language, speed, the last bandwidth estimate) go to localStorage. Upload sessions go to IndexedDB, because they must survive a reload.
The API
GET /api/watch?v=abc123 -> WatchData (also inlined in the first HTML)
GET /api/browse/home?cursor=c1 -> Page of VideoCard
GET /api/next?v=abc123&cursor=c2 -> Page of VideoCard (the rail)
GET /api/comments?v=abc123&sort=top&cursor=c3
-> { items: Comment[], next: "c4", total: 18234 }
GET /api/comments/cm9/replies?cursor=c5
POST /api/comments { videoId, text, parentId? } -> Comment
POST /api/uploads { name, size, type } -> { sessionUrl, videoId }
PUT {sessionUrl} Content-Range: bytes 0-8388607/5368709120 -> 308 Range: bytes=0-8388607
GET /api/videos/v77/status -> { state: "processing", progress: 0.42 }
PATCH /api/videos/v77 { title, description, version: 6 } -> 200 | 409 conflict
POST /api/stats/watch (sendBeacon, batched events) -> 204
Error body: { "error": { "code": "VIDEO_UNAVAILABLE", "message": "...", "retryable": false } }
Three choices in it are worth saying out loud. Every list uses cursors, for the reason the comments chapter showed. The error body has a retryable flag, so the client knows whether to back off and retry (a timeout) or show a message (a private video). And the watch endpoint returns everything the page needs in one response, shaped for this screen, instead of making the client join five resources over five round trips on a slow phone.
Check your understanding · Exercise 10
Where should each piece of state live? Select all that apply.
Watch-time analytics and playback quality
For a video site, analytics is part of the product. Creators are paid and ranked by watch time, recommendations learn from what was watched to the end, and the player team tunes ABR with stall data. So the numbers must be right, and they must arrive, even when the viewer closes the tab mid-video on a phone.
The naive client sends an event on every timeupdate (four or more per second) and a summary in the unload handler. The first floods the servers; the second is lost on mobile, where unload often never fires.
What to measure
- Watch time: seconds of video actually played, not seconds the tab was open. At 2x speed, 60 seconds of video plays in 30 seconds of wall time; a paused tab counts nothing. Add up
currentTimedeltas while inplaying, ignoring jumps bigger than about 1.5 seconds (those are seeks). - Time to first frame: from navigation (or the click, for later videos) to the first frame shown.
requestVideoFrameCallback, where supported, reports when a frame is really presented, which is more honest than theplayingevent. - Stalls: count and duration of every
bufferingstay that wasn't caused by a seek, plus the rebuffer ratio per play. - Quality: time-weighted average bitrate, number of switches, and dropped frames from
getVideoPlaybackQuality. - Failures: plays that never started, with the reason (network, decode, blocked autoplay), and exits before the first frame.
- Impressions: a card counts as seen when at least half of it is visible for a second, measured with an
IntersectionObserver.
Most of these fall out of the player's state machine for free: each transition is logged with a timestamp, and stall time is just time spent in buffering after playback began. That's one more reason the explicit machine pays off.
Heartbeats and beacons
Watch time is sent as heartbeats: small reports of the played ranges since the last one, every 10 seconds of playback at first and every 30 to 40 seconds later, since most viewers who leave do so early. Each carries a play ID (one per video load), a sequence number, and the ranges, so the server can drop duplicates and spot gaps. Quality events ride along in the same batch.
const queue: StatEvent[] = [];
function flush() {
if (!queue.length) return;
const body = JSON.stringify({ playId, events: queue.splice(0) });
// sendBeacon survives the page closing; fetch keepalive is the fallback.
if (!navigator.sendBeacon("/api/stats/watch", body)) {
fetch("/api/stats/watch", { method: "POST", body, keepalive: true });
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") flush();
});
addEventListener("pagehide", flush);
visibilitychange to hidden is the last event mobile browsers reliably deliver.visibilitychange, the moment the browser may freeze the page.sendBeacon queues a small POST that the browser delivers even after the page is gone; it has a size limit (about 64 KB in flight), so batches stay small. Never block the main thread or the video for analytics: collection is a few numbers per second, and sending happens in the background. And keep viewers' privacy in mind: no personal data in URLs, honor consent choices, and sample verbose debug logs instead of sending them for every play.
Check your understanding · Exercise 11
At 2x speed, a viewer plays 2 minutes of video (1 minute of wall time), seeks back 30 seconds, plays 1 more minute of video, then switches apps on their phone. What should the client report and when? Select all that apply.
Accessibility, languages, and weak devices
These three get their own chapter because a video site fails them in specific ways. A custom player throws away everything the browser's built-in controls gave for free, so accessibility must be rebuilt on purpose. A global audience means numbers, dates, and captions in many languages. And the median phone is far weaker than the laptop the team builds on.
An accessible player
- Every control is a real
buttonwith a label that says what it does now: Play becomes Pause, Mute becomes Unmute. Don't combine a changing label witharia-pressed; pick one. - The seek bar is a
role="slider"witharia-valuemin,aria-valuemax,aria-valuenow, and anaria-valuetextin words, like "1 minute 35 seconds of 12 minutes 5 seconds". Arrow keys move 5 seconds, Page Up and Page Down move 10 percent, Home and End jump to the ends. Volume works the same way. - Settings, quality, and speed menus follow the menu button pattern:
aria-haspopup,aria-expanded, arrow keys inside, Escape to close with focus returned to the button. - Controls fade after about 3 seconds of no movement while playing, but never while focus is inside them, or a keyboard user loses the button they're on.
- A polite live region announces changes that aren't visible otherwise: "Captions on, English", "Speed 1.5", "Quality 720p". Never announce the time every second.
- Captions on every video (automatic ones as the fallback), caption style settings (size, color, background), and audio description tracks where the creator provides them.
- With
prefers-reduced-motion, hover previews and the mini-player animation are off. Autoplay of Up next is a visible setting, not a hidden default.
Languages and regions
Format, don't concatenate. View counts use Intl.NumberFormat with notation: "compact", which gives "1.2M" in English and "1,2 Mio." in German. Ages use Intl.RelativeTimeFormat ("3 days ago"), and durations go through a locale-aware formatter.
For right-to-left languages, set dir="rtl" and build the layout with logical CSS properties like margin-inline-start, so the rail and the menus flip without a second stylesheet. Material Design's guidance for right-to-left layouts keeps media playback controls and progress bars unmirrored; whichever way you choose, decide it once and test it with native readers.
Content has its own language, separate from the interface. A Spanish viewer using the English interface may watch a Japanese video. Mark the title and description with their lang attribute so screen readers pronounce them correctly. The player data lists caption tracks and, for dubbed videos, several audio tracks by language; the player picks defaults from the viewer's preferences and remembers manual choices.
Weak devices and slow networks
Decision How should the site serve a 2019 budget phone on a slow network?
What we pick: One app that adapts. The core path (shell, player boot, first segment) is small for everyone, under about 150 KB of compressed script, and the extras are switched off by tier. Weak devices get the same product with fewer luxuries, not a different product.
Concretely, on a weak device: hydrate the rail and comments only when they near the viewport; keep long tasks off the main thread during startup so the play button responds (INP); run the MSE pipeline in a dedicated worker where Chromium supports it, so a busy page can't starve the video; cap quality at 480p when dropped frames say so; and prefer H.264 when mediaCapabilities says AV1 isn't power efficient.
On a slow or flaky network: start at 240p, give segment requests a timeout and two or three retries with backoff, and switch to a backup CDN host listed in the manifest after repeated failures. With Save-Data on, there are no previews and no prefetching.
Check your understanding · Exercise 12
A screen reader user tabs to the seek bar of a 12-minute video, 95 seconds in. What should they hear, and what should work? Select all that apply.
Follow-ups: live streams and chat, ads, and downloads
With the core design on the board, interviewers push on the edges. Each of these three could be its own interview; what they want here is to see that your design bends to fit them, and that you know the one or two decisions each one forces.
Live streams
A live stream uses the same segments and manifests, with one change: the playlist never ends. The player reloads it about every segment duration to find new segments, and the server keeps a sliding window of recent ones so viewers can rewind (a DVR window).
The big new number is latency: the delay between the camera and the viewer. Classic HLS buffers about three 6-second segments, so viewers are 20 to 30 seconds behind. Low-Latency HLS and low-latency DASH send parts of segments as they're encoded and get to 2 to 5 seconds.
Lower latency means a smaller buffer, and a smaller buffer means more stalls. So the creator picks a mode (normal, low, ultra-low) depending on whether chat interaction or smoothness matters more. The player holds a target distance from the live edge; after a stall it plays at about 1.05x to catch up gently instead of jumping.
ABR changes too: with a buffer of 3 seconds, the buffer rule has nothing to work with, and segments that arrive as they're encoded download at the speed of the encoder, not the network, so throughput must be measured from the bursts within each chunk.
Chat is the harder part of the page. A big stream gets thousands of messages a second, far more than anyone can read or the DOM can hold. The server samples (a "Top chat" view) and sends batches over a WebSocket; the client renders at most a few times a second in one batch, keeps only the last 200 or so messages in the DOM, and stops autoscrolling when the viewer scrolls up, with a "new messages" button to return.
Sending is rate-limited, and a slow mode can enforce one message per N seconds. For replays, chat messages carry video timestamps and are shown in sync with currentTime.
Ads
Decision How should ads get into playback?
What we pick: On the web, client-side insertion with a hard timeout is the usual starting point: measurement is rich and the content stays cacheable. Server-side insertion is worth it where blocking or devices without SDKs dominate. Either way, ads become extra states in the player's machine (ad-loading, ad-playing), the seek bar is locked during an ad, and a failed or slow ad request (over about 1.5 seconds) must fall through to the content, never block it.
Offline downloads
Downloads for offline viewing reuse the segments too. The viewer picks a quality; a download manager fetches that rendition's init segment, every media segment, and the captions, and stores them in the Cache Storage API or IndexedDB. A service worker answers the player's segment requests from storage when offline, so the player itself barely changes.
Before starting, navigator.storage.estimate() checks that the download fits (10 minutes at 720p is about 210 MB), and navigator.storage.persist() asks the browser not to evict them under pressure. Chromium's Background Fetch API lets a long download continue after the tab closes.
Rights add rules: downloads usually expire unless the device comes online every few weeks, and paid content is encrypted, with Encrypted Media Extensions and a persistent license where the key system supports it. The UI needs states for all of it: queued, downloading with progress, ready, expiring soon, and expired.
Check your understanding · Exercise 13
A creator streams a Q&A and wants viewers' chat questions to match what's on screen. Which changes help? Select all that apply.
Running it in the interview
You now know far more than fits in 45 minutes. The skill is choosing what to say, in what order, and showing the reasoning behind each choice. A strong answer picks two or three deep areas (usually the watch page's first load, adaptive streaming, and the mini-player) and touches the rest in a sentence each.
| Minutes | What to do | What to draw |
|---|---|---|
| 0 to 5 | Scope questions; agree on watch page, player, mini-player, and one of upload or home grid. Write the four target numbers. | A short list of screens and the numbers in a corner |
| 5 to 12 | The architecture: shell, player host, pages, data cache, CDN, API, edge render. | The architecture diagram, video bytes going straight to the CDN |
| 12 to 20 | First load: streaming render, inline player data, preconnect, poster as LCP. | The first-load sequence |
| 20 to 30 | Adaptive streaming: segments, manifest, MSE, ABR rules, startup bet, caps. | Ladder and buffer with the ABR zones |
| 30 to 37 | Player states and the mini-player in the shell. | The state machine, then the slot and the corner |
| 37 to 45 | Whichever the interviewer picks: upload, comments, analytics, or a follow-up. | The resumable upload sequence, or the lazy comments sketch |
Say the forces out loud before the solutions: video is 99 percent of bytes, the network changes mid-play, a stall is worse than blur, and the player outlives pages. Then each decision sounds like a consequence instead of a recitation. When you pick an option, name the one you rejected and the scenario that breaks it, with a number: "a 1080p segment over 1 Mbps takes 20 seconds, so trusting an old estimate stalls the viewer for 8."
Think first
The interviewer asks: "If navigation between videos is client-side, how do search engines see video pages?" What do you answer?
Every video URL also works as a first load. A crawler, or anyone opening a link, requests /watch?v=ID directly and gets the full server-rendered page with metadata, Open Graph tags, and VideoObject data.
Client navigation is only an optimization for people already in the app. The same URL, two ways to arrive, same content.
Think first
"Half our viewers are now on smart TVs." What changes in your design?
TVs are weak CPUs on big screens: the 4K rung matters, but JavaScript must be lean and animations few. Input is a remote, so every control must work with arrow keys and focus must always be visible (spatial navigation).
Many TV platforms lack full MSE features or ad SDKs, which pushes toward native players or server-side ad insertion. The mini-player disappears; the rail becomes a row under the video.
Check your understanding · Exercise 14
You have 8 minutes left and haven't covered uploads. The interviewer asks about them. Which answer uses the time best? Pick one.
Summary
- Video is over 99 percent of the bytes, so the watch page is built around one goal: the first frame. Inline the player data, preconnect to the CDN, and make the poster the LCP element.
- Render the first load on the server and stream it; navigate on the client after that. That gives search engines real pages and keeps one player alive.
- Adaptive streaming cuts each rendition into short segments that start with a keyframe. A script player on Media Source Extensions picks the rendition for every segment and evicts what's behind the playhead.
- Use throughput to choose quality at startup and the buffer once playing, with a safety factor, emergency drops, and caps from screen size and dropped frames. A stall is worse than blur.
- Model the player as one explicit state machine plus the viewer's intent. The UI and the stall metrics both read from it.
- The app shell owns the single player and positions it over a slot or in the corner. The element never leaves the document, so the mini-player never restarts.
- Prefetch by intent: small JSON on hover, one media segment for Up next. Load comments only when the viewer nears them, and page them with cursors.
- Send big uploads as a resumable session in chunks, with the server as the source of truth for the offset, and save metadata to a draft video while it processes.
- Count watch time as seconds of video played, send heartbeats, and flush with
sendBeaconwhen the page is hidden.
Check your answer
Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.
References
- Media Source API MDN: MediaSource, SourceBuffer, and ManagedMediaSource.
- RFC 8216: HTTP Live Streaming The HLS playlist format: master (now called multivariant) and media playlists, segments, and live.
- Media Capabilities API Asking whether a codec and size will decode smoothly and power-efficiently.
- Media Session API Lock-screen metadata and hardware media keys.
- Picture-in-Picture API Floating the video above other windows with requestPictureInPicture.
- Navigator: sendBeacon() method Sending analytics that survive the page closing.
- Page Lifecycle API Why to flush on visibilitychange and pagehide, not unload.
- Fast playback with audio and video preload web.dev on preloading media and getting to the first frame quickly.
- Slider pattern, WAI-ARIA Authoring Practices Keyboard and ARIA rules for the seek and volume sliders.
- Shaka Player An open source MSE player for DASH and HLS; its ABR and buffering code is worth reading.
- tus: resumable file uploads An open protocol for resumable uploads, with the same offset idea as this design.
- YouTube Data API: resumable uploads Google's session, Content-Range, and 308 Resume Incomplete flow, step by step.
Concepts behind it
- Streaming server renderingThe server sends a page's HTML in pieces as each part becomes ready, instead of waiting for its slowest data: the layout paints right away, slow sections stream in later, and each piece hydrates on its own.
- Client-side routingHow single-page apps change screens without reloading: matching URLs to nested layouts, loading each route's code and data in parallel, guarding private pages, and restoring scroll and focus.
- Prefetching and preloading dataStarting the fetch for the data and code of the next screen before the user asks for it, on hover, on sight, or on a guess, so the click feels instant without wasting the user's data plan.
- Images and mediaSending each user an image of the right size and format, reserving its space, and loading it at the right moment: images are usually the largest share of a page's bytes and its LCP element.
- Uploading filesHow a file gets from the user's device to your storage: through your server or straight to object storage with a pre-signed URL, in resumable chunks with honest progress, checked on both sides, and served back without opening a security hole.
- State machines for UIModel a flow as a fixed set of states and the events that move between them, so impossible combinations like "loading and failed at once" can't happen and every button does the right thing in every state.
- Product analyticsRecording what users do as well-named events that follow a tracking plan, delivered reliably even offline, with consent respected, and checked so that the numbers people decide with are true.
- Core Web VitalsThree numbers, measured on real users, that say whether a page loads fast (LCP), reacts fast (INP), and stays still (CLS): what each one measures, what makes it bad, and how to fix it.
Comments below the fold
A popular video has 200,000 comments, and most viewers never scroll down to them. Yet the naive watch page fetches the first 20 threads with everything else, renders them on the server, and ships the comment code in the main bundle. Each viewer pays about 40 KB of HTML, 60 KB of JavaScript, and 20 avatar images for a section most will never see, and the server waits on the comments service before it can finish the page.
Decision When should the comments section load?
With the page
Fetch and render the first page of comments during the server render.
Pros
Cons
After the page is idle
Once the video is playing, use
requestIdleCallbackor a timer to load comments in the background.Pros
Cons
When the viewer gets close
Place a small sentinel element where comments begin. An
IntersectionObserverwith arootMarginof about 800 px fires when it nears the viewport, and only then the comments code and first page load together.Pros
Cons
What we pick: Load when the viewer gets close. The comment count comes with the watch data so the header can show it early, and the code chunk plus the first page of 20 threads load when the sentinel comes within about 800 px. It costs nothing for the majority and is ready in time for nearly everyone else.
Paging uses a cursor, an opaque token the server returns with each page, not page numbers. New comments arrive every second on a busy video; with
offset=20, the second page would repeat comments that moved down, or skip ones that moved up. Each sort order (Top and Newest) has its own cursor and its own list, kept in memory, so switching back and forth doesn't refetch. Replies load only when the viewer expands "12 replies", with their own cursor.Posting is optimistic. The new comment appears at the top at once, marked as sending; the server's reply swaps in the real ID. On failure, the comment stays with the typed text and a Retry button, never silently lost. Two details connect comments to the player: timestamps in text, like
4:32, become buttons that seek the shell's player; and all comment text is rendered as text nodes with parsed links, never as HTML, because comments are untrusted input from strangers.Check your understanding · Exercise 8
A viewer reads page 1 of Newest comments. While they read, 30 new comments are posted. They click Show more. What happens with each paging scheme? Select all that apply.