AlgoInsight

Free lesson · Frontend System Design

Why your bundling strategy depends on a protocol decision you didn't make

Medium10 min readfrontendsystem-design

Here's a claim that sounds wrong until you trace it: whether you should bundle your JavaScript into one big file or split it into many small ones depends on which version of HTTP your server speaks. The same app, same code, can have opposite optimal strategies on HTTP/1.1 versus HTTP/2. If that connection isn't obvious, you're in the right place - because the evolution of HTTP from 1.1 to 2 to 3 isn't trivia about the plumbing. Each version solved a specific bottleneck, and each solution changed what frontend engineers should do. Understanding the why behind the versions lets you derive the right optimization strategy instead of memorizing rules that may not even apply to your stack.

The thread running through all three versions is a single enemy with a memorable name: head-of-line blocking - the situation where one stuck item blocks everything queued behind it. Each HTTP version attacks head-of-line blocking at a different layer, and once you see that, the whole evolution snaps into a clear story.

HTTP/1.1: persistent connections, but a blocking problem

HTTP/1.1 was a huge improvement over 1.0 because it introduced persistent connections (keep-alive): instead of opening a fresh TCP connection - with its handshake cost - for every single request, the browser could reuse one connection for multiple requests. Good. But it had a crippling limitation: on a single connection, requests are handled one at a time, in order. Request two can't be answered until request one finishes. This is application-layer head-of-line blocking - a slow response holds up everything behind it on that connection.

Browsers worked around this by opening multiple parallel connections to the same origin - typically capped at around six. So you got six lanes of traffic instead of one. But six is a hard ceiling, and each connection carries its own handshake overhead. When a page needed dozens of resources - and pages did, with all their scripts, stylesheets, images, and icons - six lanes weren't enough, and resources queued.

This constraint birthed an entire era of frontend optimization techniques whose only purpose was dodging the request limit:

Every one of those was a hack to survive HTTP/1.1's blocking and connection cap. Hold that thought, because the next version makes most of them obsolete.

graph TD
    subgraph H1["HTTP/1.1"]
        C1[~6 parallel connections per origin]
        C1 --> B1[Each connection: one request<br/>at a time, in order]
        B1 --> HOL1[Slow response blocks the<br/>queue behind it]
        HOL1 --> Hacks[Workarounds: bundling,<br/>sprites, domain sharding]
    end
This diagram shows HTTP/1.1's model and its consequences. The browser opens about six parallel connections per origin. On each connection, requests are handled one at a time, in order. A slow response blocks the queue behind it, which is application-layer head-of-line blocking. To work around this, frontend engineers used techniques like bundling everything into few files, combining icons into image sprites, and domain sharding across subdomains to exceed the six-connection limit.

HTTP/2: multiplexing kills application-layer blocking

HTTP/2's headline feature is multiplexing: many requests and responses can be in flight simultaneously over a single connection, interleaved as independent streams. Request two no longer waits for request one - they share the pipe concurrently. This eliminates the application-layer head-of-line blocking that defined HTTP/1.1, and it does so over one connection, removing the need for six parallel ones.

The consequences for frontend optimization are direct and they invert old advice:

HTTP/2 also added header compression (HPACK), since repetitive headers (cookies, user-agent) sent on every request were real overhead, and a binary framing layer that's more efficient to parse than 1.1's text. It originally included server push (the server proactively sending resources it predicts you'll need), but that proved hard to use well and has been largely deprecated in favor of resource hints like preload.

But HTTP/2 had a remaining weakness, and naming it sets up the final version: multiplexing solved blocking at the application layer, but all those streams still ran over a single TCP connection - and TCP itself guarantees in-order delivery. So if a single packet is lost, TCP holds back all the streams until that packet is retransmitted. The blocking moved down a layer: HTTP/2 has TCP-level head-of-line blocking. On a clean network you never notice. On a lossy mobile connection, you do.

HTTP/3 + QUIC: solving blocking at the transport layer

HTTP/3 makes a radical move: it abandons TCP entirely and runs over QUIC, a new transport protocol built on UDP. Because QUIC implements its own stream multiplexing with independent streams, a lost packet only blocks the one stream it belongs to - the others keep flowing. This finally eliminates head-of-line blocking at the transport layer, which is exactly the weakness HTTP/2 still had.

QUIC brings two more wins that matter especially for the slow, unstable mobile networks the first module kept emphasizing:

graph LR
    H1["HTTP/1.1<br/>1 request at a time<br/>per connection"] -->|multiplexing| H2["HTTP/2<br/>many streams,<br/>1 TCP connection"]
    H2 -->|QUIC over UDP| H3["HTTP/3<br/>independent streams,<br/>no TCP HOL blocking"]
    H1 -.->|enemy| E1[App-layer<br/>HOL blocking]
    H2 -.->|enemy moved down| E2[TCP-layer<br/>HOL blocking]
    H3 -.->|enemy defeated| E3[No HOL blocking<br/>+ faster setup<br/>+ connection migration]
This diagram traces HTTP's evolution as a battle against head-of-line blocking. HTTP/1.1 handles one request at a time per connection and suffers application-layer head-of-line blocking. HTTP/2 introduces multiplexing - many streams over one TCP connection - which defeats application-layer blocking, but the enemy moves down to TCP-layer head-of-line blocking, because a lost packet stalls all streams. HTTP/3 runs over QUIC on UDP with independent streams, so a lost packet blocks only its own stream, defeating head-of-line blocking entirely, while also adding faster connection setup and connection migration across network changes.

The comparison, and what it means for your decisions

HTTP/1.1 HTTP/2 HTTP/3
Transport TCP TCP QUIC (UDP)
Concurrency ~6 connections, 1 req each Multiplexed streams, 1 connection Multiplexed streams, 1 connection
Head-of-line blocking Application layer TCP layer None
Header handling Plain text, repeated Compressed (HPACK) Compressed (QPACK)
Connection setup TCP + TLS handshakes TCP + TLS handshakes Combined, often 1-RTT / 0-RTT
Survives network switch No No Yes (connection migration)
Frontend implication Bundle aggressively, shard domains Granular files OK, don't shard Same as H2, better on mobile

Important: The single most interview-relevant takeaway: the right bundling strategy depends on your HTTP version. On HTTP/1.1, minimize requests - bundle everything. On HTTP/2 and HTTP/3, many small requests are cheap, so prefer granular code splitting for better caching, and never domain-shard. If an interviewer asks "should you bundle everything into one file?", the senior answer is "it depends on the protocol - on HTTP/2+ I'd split for cache granularity; the bundle-everything rule was an HTTP/1.1 workaround." That conditional answer shows you understand the mechanism.

Interview: A common follow-up: "what's the benefit of HTTP/2 over 1.1?" Don't just say "it's faster." Say: "Multiplexing - many concurrent streams over one connection - which eliminates the application-layer head-of-line blocking and the six-connection limit, so the old bundling and domain-sharding hacks become unnecessary or even harmful. HTTP/3 goes further by moving to QUIC over UDP, removing the TCP-level blocking that HTTP/2 still had and adding connection migration for mobile." Framing each version as defeating head-of-line blocking at a deeper layer is the structure that signals real understanding.

The practical realities of HTTP versions

Knowing the theory is one thing; knowing how it actually plays out in your work is another, and a few practical points round out a senior understanding.

You don't usually choose the version - it's negotiated. When a browser connects, it and the server negotiate the highest version both support (HTTP/2 and HTTP/3 negotiate via TLS extensions and, for HTTP/3, an Alt-Svc header that advertises QUIC availability). The practical consequence is that adopting a newer protocol is mostly an infrastructure decision rather than an application one: you upgrade your server or, more often, front your assets with a CDN that already speaks the latest versions, and your existing frontend code transparently gets the benefit. So you don't write code that "uses HTTP/2." You enable it at the server or, far more commonly, you put a CDN in front of your assets and the CDN speaks HTTP/2 and HTTP/3 to users automatically. This is why "use a CDN" is partly a protocol-upgrade decision: modern CDNs give your users HTTP/3 with zero application changes.

HTTP/2 and HTTP/3 effectively require HTTPS. Browsers only negotiate them over TLS. This is one more reason HTTPS is non-negotiable - it's not just security (covered in the security module), it's the gateway to the faster protocols. A site stuck on plain HTTP is also stuck on HTTP/1.1's limitations.

You can verify the version in DevTools. The Network panel has a "Protocol" column (you may need to enable it by right-clicking the column header) showing h2, h3, or http/1.1 per request, so you can confirm at a glance what each asset actually loaded over rather than assuming. When reasoning about a real site's performance, checking what protocol its assets actually load over is a concrete diagnostic - a site bundling aggressively while already on HTTP/2 may be optimizing for a constraint it no longer has.

The gains are real but not magic. HTTP/2 multiplexing helps most when a page loads many resources; for a page with a handful of large files, the benefit is smaller. HTTP/3's wins concentrate on lossy, high-latency networks - exactly mobile - and are less dramatic on a fast wired connection. So the value of upgrading depends on your users' conditions, which (recurring theme) is the frontend's defining concern.

What none of these versions can do - and why the module has a second half

Step back and notice something all three HTTP versions share, because it motivates the entire second half of this module: they are all fundamentally request/response. The client asks, the server answers. No matter how clever the multiplexing or how fast the handshake, the client always initiates. The server cannot, on its own, decide "something changed, let me tell the connected clients" and push data out. HTTP has no native concept of the server starting a conversation.

That's fine for most of the web - you click, you get a page; you submit a form, you get a result. But it's a fundamental limitation for an entire class of features: a chat message someone else sent, a stock price that just ticked, a collaborator's cursor moving, a notification firing. For all of those, the server is the one with new information, and request/response forces an awkward dance - the client has to keep asking "anything new? anything new?" because the server can't volunteer it.

graph LR
    subgraph RR["Request/Response (all HTTP versions)"]
        Client1[Client] -->|always initiates| Server1[Server]
        Server1 -.->|can only respond,<br/>never initiate| Client1
    end
    RR -->|"but what if the SERVER<br/>has new data first?"| Gap[The realtime gap]
    Gap --> Next[Polling, Long Polling,<br/>WebSockets, SSE, WebHooks]
This diagram highlights the shared limitation of all HTTP versions. In the request-response model, the client always initiates and the server can only respond, never initiate. This creates the realtime gap: what happens when the server has new data first, like an incoming chat message or a stock tick? The answer is the set of realtime techniques covered next - polling, long polling, WebSockets, server-sent events, and webhooks - which are the various ways to work around or escape the request-response limitation.

This limitation is precisely what the realtime half of the module exists to address. The techniques there range from faking server push by having the client poll repeatedly, to genuinely escaping request/response with persistent bidirectional connections. So the HTTP versions you just learned are the foundation for the request/response API styles coming next (REST, GraphQL, gRPC), and the limits of HTTP request/response are the motivation for the realtime mechanisms after that. Both halves trace back to this protocol foundation.

Interview: Two HTTP-version questions show up repeatedly, and crisp answers mark you out. "Why is HTTP/2 faster than 1.1?" → "Multiplexing eliminates the application-layer head-of-line blocking and the six-connection limit, so the old bundling and sharding hacks become unnecessary." "Do you still need to bundle everything on HTTP/2?" → "No - granular files are better for caching, and bundling was largely an HTTP/1.1 workaround for the request limit." Both answers tie the protocol to a frontend decision, which is the level interviewers are listening for. Reciting version features without connecting them to what you'd actually do is the weaker answer.

What you should carry out of this lesson

HTTP evolved by attacking head-of-line blocking at successively deeper layers. HTTP/1.1 had application-layer blocking and a six-connection limit, which spawned bundling, sprites, and domain sharding as workarounds. HTTP/2's multiplexing defeated application-layer blocking over a single connection - making those workarounds unnecessary and domain sharding actively harmful, and shifting the optimal strategy toward granular files for better caching. HTTP/3 over QUIC eliminated the remaining TCP-layer blocking and added faster setup and connection migration, which especially helps mobile users on unstable networks.

That's the transport. Now we move up to the API styles built on top of it - the actual contracts between client and server. We start with the one that's been the default for over a decade and remains the right choice more often than its critics admit: REST, examined specifically through the frontend's eyes.

Back to Frontend System Design