HTTP/2 streams can be prioritized to influence the order in which resources are loaded, preventing critical elements from being blocked by less important ones.
Let’s watch this happen in real-time. Imagine a browser requesting an HTML document, a critical CSS file, a large JavaScript bundle, and a decorative image. Without prioritization, the browser might start fetching the image before the CSS, delaying the rendering of the page.
Here’s how that looks in a simplified trace, showing requests and their order:
1. GET /index.html (priority: 0, weight: 256)
2. GET /styles.css (priority: 1, weight: 256)
3. GET /app.js (priority: 2, weight: 256)
4. GET /logo.png (priority: 3, weight: 256)
In this default scenario, the browser might actually start downloading app.js or logo.png before styles.css has fully downloaded, depending on network conditions and server processing. The priority field here is just an internal index, not the actual prioritization signal. The weight is more relevant, with higher weights indicating higher priority.
HTTP/2 introduces a mechanism for clients (browsers) to signal to servers how they’d like resources to be handled. This isn’t just about what to fetch, but when and how urgently. The core of this mechanism lies in the PRIORITY frame, which can be sent along with a request or independently to modify the priority of an existing stream.
The key parameters you can control are:
- Stream Dependency: A stream can depend on another stream. If stream A depends on stream B, stream B’s data must be "processed" (generally meaning acknowledged or partially sent) before stream A can make significant progress.
- Weight: Each stream has a weight (1-256). A higher weight means a larger proportion of bandwidth and processing time will be allocated to that stream when multiple streams are competing.
- Exclusive Flag: If set, the dependent stream gains exclusive access to resources until it’s completed or relinquishes control.
Let’s re-evaluate our example with explicit prioritization. The browser knows index.html is essential for initial rendering, styles.css is critical for layout, and app.js is needed for interactivity, while logo.png is purely decorative.
The browser might send the following sequence of frames:
-
HEADERSframe for/index.html:- Stream ID: 1
- Priority: None (root stream)
- Weight: 256 (default, highest)
-
PRIORITYframe (orHEADERSwith priority info) for/styles.css:- Stream ID: 3
- Depends On: 1 (depends on
index.html’s completion) - Weight: 256
- Exclusive: 0
-
PRIORITYframe (orHEADERSwith priority info) for/app.js:- Stream ID: 5
- Depends On: 1 (depends on
index.html’s completion) - Weight: 128 (lower than CSS)
- Exclusive: 0
-
PRIORITYframe (orHEADERSwith priority info) for/logo.png:- Stream ID: 7
- Depends On: 1 (depends on
index.html’s completion) - Weight: 1 (lowest)
- Exclusive: 0
With this, the server understands: index.html is paramount. Once we start sending index.html, we can then consider styles.css and app.js as having equal importance but less than index.html, and logo.png is the lowest priority. The server’s scheduler will then try to dole out network capacity and processing time accordingly. It will likely send chunks of index.html, then styles.css, then app.js, and only sprinkle in logo.png data when there’s spare capacity.
This allows the page to become visually ready much faster, even if the total download time for all assets is the same. The user perceives a faster load because the critical path is unblocked.
The actual implementation of prioritization by servers can vary. Some might strictly adhere to weights, while others might have heuristics to adapt based on observed stream progress or resource types. The browser also plays a role, as it can dynamically adjust priorities as the page loads and new resources are discovered or their importance changes. For example, if a user interacts with the page, JavaScript streams might suddenly gain higher priority.
The trickiest part of HTTP/2 prioritization is understanding how the server interprets and acts upon these frames. Not all servers implement the full flexibility of the PRIORITY frame, and some might only consider the weight. Furthermore, the dependency chain can become complex. If stream A depends on B, and B depends on C, then C must be substantially complete before B can proceed, which in turn must be substantially complete before A can proceed. This creates a tree-like structure of resource dependencies that the server’s scheduler must navigate.
The next hurdle is understanding how to debug and inspect these prioritization signals in practice.