Today, we encountered a huge problem with Chrome’s Service Worker implementation in combination with HTTP/2.

As long as Developers cannot specify priorities in the fetch() API there is a huge performance problem with Service Workers.

The Issue

Requests lose all HTTP/2 stream dependencies when requested through a Service Worker with extreme effects on page load time.

Discussion in Twitter

The issue triggered a lot of interesting discussion on twitter.

Explaining the Issue

To illustrate that, we built a stripped-down website, which loads a CSS fileand 80 small red images. The CSS imports another CSS with fonts in it.

Without a Service Worker, the site loads as follows:

the HTML is loaded. the style.css and all the image request are fired in parallel, with the CSS having the highest priority. When the style.css has finished loading, the font.css request is fired with a very high priority. After the font.css finishes loading, the page is painted for the first time (even though some of the images have not finished loading, yet).

The waterfall without the Service Worker, however, looks as expected. Obviously, importing one CSS in another is an anti-pattern, but it is very good for illustrating the issue we are about to encounter.

The actual problem starts, when we add a very simple Service Worker to the page which only forwards requests to the network:

addEventListener('fetch', event => {

event.respondWith(fetch(event.request));

});

With the Service Worker installed, the page loads similarly to before: After the HTML, the style.css is requested in parallel with all the images with the CSS having the highest priority. When the style.css finishes loading, the fonts.css is requested. The problem: the style.css is loaded with the lowest priority of the all the requests, even though it blocks rendering and is only 0.6 KB in size.

The HTML is loaded. the style.css and all the image request are fired in parallel, with the CSS having the highest priority. When the style.css has finished loading, the font.css request is fired. The problem: the font.css is loaded with the lowest priority of all the requests, even though it blocks rendering and is only 0.6 KB in size.

The effect is that the render blocking CSS is loaded last and therefore delays the paint from 3 seconds to over 6 seconds:

The request details in the waterfalls clearly show that the HTTP/2 stream dependencies and priorities are lost when the requests are proxied through the Service Worker.

Without the Service Worker, the CSS HTTP/2 stream depends on stream 0, which is the HTML request. This means the CSS is loaded in parallel with all the images.

With the Service Worker, the CSS stream depends on stream 169, which is the last image. This means the CSS is loaded as the very last resource.

HTTP/2 stream dependency and weight without Service Worker

HTTP/2 stream dependency and weight with Service Worker

In summary, Chrome prioritizes all requests that are proxied through the Service Worker, in the order, they are fetched in the Service Worker. I.e. request 5 depends on request 4 depends on request 3 and so on.

Firefox

Firefox appears to have a similarly problematic approach. Every request that is proxied through the Service Worker gets the same priority that looks like this:

HTTP/2 Stream: X, weight 22, depends on 11

Sadly since the release of the new WebPagetest agent, we cannot see the HTTP/2 priorities in the waterfalls, but still, here they are without Service Worker and with Service Worker.

Conclusion

We are big fans of HTTP/2 and multiplexing, but correct resource dependencies and weights are essential. The issue appears to be described briefly in this paper in section 4.4.

In case we missed something or there is another way to avoid losing request priorities, please give us a hint, it would be highly appreciated. We have filed a bug report for this issue at https://bugs.chromium.org/p/chromium/issues/detail?id=872776.