5 ms·
I agree these things can be useful, but again none of these come out of the box (if I haven't overlooked or misunderstood something). Including "doing the naive
by tannhaeuser 6y ago
I agree these things can be useful, but again none of these come out of the box (if I haven't overlooked or misunderstood something). Including "doing the naive thing"; I mean how do you expect your web server to automatically push CSS or SVG sprites/fonts unless you're relying on the server to intercept/parse your HTML for delivery-time optimizations a la PageSpeed and make heuristic scheduling decisions? Unless you're putting in the effort to optimize your payloads you will just result in as many roundtrips with HTTP/1.1 + keep-alive.
- derefr 6y agoWeb browsers are limited in the number of concurrent socket connections they'll open to a given origin. This matters not-at-all in HTTP/2, since everything is going over a single socket; while mattering quite a lot in HTTP/1, since dependent resources being loaded in parallel must be loaded on separate sockets. If you only have six parallel sockets to work with, then if your page is, say, an image gallery, then the Javascript file that makes it work (loaded at the bottom of the body) might be blocked waiting behind the loading of e.g. some large image higher up in the body. The previous requests need to entirely finish (= a round trip) before the next requests on the same socket can start. Keep-alive does nothing to fix that. HTTP/1.1 pipelining partially mitigates this, allowing the client to "queue up" a list of all the dependent resources it wants from each socket; but it suffers from head-of-line blocking. Which sounds like some arcane thing, but in practice it means that big things might block the loading of small things. (The browser doesn't know how big things are, so it can't effectively schedule them; and the server must dumbly queue results up in the same order the client requested them, because that's the only way the HTTP pipeline's implicit flow sequence counters will match up.) HTTP/2 is a full mitigation for this problem, since—even without a heuristic "prioritization strategy" for the delivery of dependent-resource flows—the "oblivious" strategy is still a good one: if you attempt to deliver all the resources in the queue concurrently; and you do so by delivering one fixed-size chunk of each flow per iteration, in a round-robin fashion; then you'll end up finishing delivery of resources smallest-to-largest—which means you'll usually deliver the most-critical resources first, no matter where in the dependent-resource queue they started. Or, in short: HTTP/1.0 = O(N) required roundtrips for a page with N resources. HTTP/1.1 with pipelining = O(1) required roundtrips for a page with N resources (followed by O(N) bytes streamed half-duplex), but the page can still take nearly the same amount of time to become interactive as if it were O(N) roundtrips, because of effectively worst-case scheduling. HTTP/1.2 = O(1) RTTs + O(N) half-duplex bytes for N resources, loading "intelligently" such that the page becomes interactive in O(log N) time.
- tannhaeuser 6y agoI really appreciate your going into these details, but I still don't understand the "oblivious" strategy thing which supposedly improves baseline performance OOTB when the criterion is the time to first render of above-the-fold content given otherwise same conditions. You say the effect of delivering all resources concurrently is that "the most-critical resources [are delivered first], no matter where in the dependent-resource queue they started". But this isn't proven; it's just a different heuristic to apply to traffic shaping assuming small resources are needed early vs assuming dependent resources (+ HTML markup itself) are loaded asynchronously in the order the browser parses an HTML DOM (the default behaviour, and the one authors can influence directly). You're not magically increasing the bandwidth by multiplexing so something has to give.
- derefr 6y agoThe "oblivious approach" is "just a different heuristic to apply", yes. It's just one that happens to work especially well for HTML rendering, given that large resources tend to be depended upon in a way (e.g. an img or video tag) that gives them a pre-download bounding-box size, allowing the rest of the page to render around them and to not reflow once they're loaded; while the types of resources that tend to be small (like CSS or JS files) are mostly depended on with the semantics that they can potentially reflow the page entirely when they finish loading, which means the browser completely inhibits interactivity (and/or rendering, depending on the browser) until those resources finish loading. The only things that kind of break this heuristic are: • large single-file Javascript SPAs. These are usually just marked with the `async` attribute, such that the initial DOM of the page can first render, then be gradually enhanced when the SPA loads. But with HTTP/2 + ES6, you can also just not pack the SPA into a single file, instead relying on ES6 modules, which will each be individually smaller and therefore will end up being delivered first by the content-oblivious round-robin chunk-delivery strategy. • web fonts, which are large and will necessarily cause a complete reflow of the page once loaded. Currently, browsers make a special loading-precedence exception for web fonts; though it doesn't matter as much right now, as they're still mostly served from third-party CDNs rather than as first-party HTTP/2 flows. AFAIK, for regular web-page retrieval, these are exactly the total set of things that currently benefit from being server-pushed along with the first response; everything else just gets handled well even without server-push. (If you're curious, server-push is really designed for the use-case of pushing secondary API responses along with an initial API response; to allow for GraphQL-like "single-round-trip resource-hierarchy/-graph walking" in a way that's more friendly for caching layers than comingling the results into a single resource-representation. It makes the most sense in a Firebase-like system, where un-asked-for server-pushed resources can be obliviously accepted and dumped into the client-side in-memory set of synced entities asynchronously to the parsing of the initial response; and then, once the dependency is parsed out on the client side, the client can discover that it already has the entity it wants in its in-memory entity store, and doesn't even need to make the request.)
- pimterry 6y agoThese do come out of the box. The advantages listed above all come from the fact that HTTP/2 runs all traffic over a single multiplexed TCP connection, rather than HTTP/1.1, where clients are forced to open many independent TCP connections (because each is a single channel where responses must be returned sequentially, in the order they were requested, and so block one another). That happens totally automatically, there's nothing special you need to do to enable that. There are also other potential advantages once you look at push etc, but using TCP as it was meant to be used really does give you many advantages for free.
- marcusarmstrong 6y agoThat's not true at all. HTTP/2 is hugely impactful without using push at all—multiplexing beyond 4/8 streams enables radically different, performant bundling strategies, all "for free" with zero configuration, just by the act of using the protocol.