Most web performance tools and resources recommend prioritizing above-fold CSS delivery to unblock streaming rendering as early as possible. Linked CSS suffers from a late start (only after the HTML head is loaded), and contention with the parallel HTML load. Inlined CSS avoids this, immediately unblocking the browser to progressively render HTML as it arrives.
Our own results from T113066#1893866 corroborate the huge influence on first paint, especially on slow connections:
Strategy / content type | First paint @"fast 2g", 840ms rtt | First paint @"slow 2g", 1300ms rtt |
Obama with images & external styles | 24.6s | 92.5s |
Obama without images, with external styles | 29s | 52s |
Obama without images and styles | 4.5s | 8.2s |
Obama without images, inline styles | 5s | 11.3s |
Obama with images, inline styles | 5s; Note deferred below-fold image loading. | 11.4s |
Obama lead section only, slim loot HTML | 6.2s | 10.6s |
Obama full page, slim loot HTML | 6s | 19.8s |
A surprising (but tangential) result is that Chrome already seems to defer loading of below-fold images, at least if CSS is available to determine above / below fold status. Time to a rendered & interactive first screen is almost unaffected by image loading if CSS is inlined or generally loaded before above-fold images start loading.
On a Galaxy Note 3 using a wifi connection, Chrome renders the first screen of Obama with images and inline styles after about a second. The full page load takes about six seconds. CPU does not seem to be a bottleneck for first paint on this ~2 year old device. Scrolling is smooth all the way through the rendering phase.
Optimizations: Only inline above-fold CSS
There is a good variety of tools available that automatically separate above-fold from below-fold styles. One of them is Google's PageSpeed module.
A likely issue with these dynamic solutions is going to be performance. It might make sense to use them as a starting point for a static split of above-fold vs. below-fold CSS instead.
Alternative approaches for early CSS delivery
HTTP/2 push
There are some early cache-aware HTTP/2 push implementations, but implementing this in our current infrastructure does not seem to be very straightforward. Nginx does not support push directly yet, which means that we'd need to use another HTTP2 frontend like nghttp2. It seems likely that nginx will gain support for HTTP2 push in the medium term as well.
ServiceWorker CSS caching / injection
Repeat requests can be sped up by persistently caching & quickly delivering CSS from a ServiceWorker. However, this won't address the large percentage of occasional visits or clients without ServiceWorker support, so can only be seen as a complementary optimization to inlining or HTTP push.
Proposal
Given the fairly low complexity of a minimal implementation & the very significant performance gains, I think we should look closely at applying inlined above-fold styles across the board.
For a production deploy, we should investigate how much size we can save with a static above-fold / below-fold RL module split. While even simple inlining is a big gain on 2g, there is a chance that the currently ~16kb extra compressed response size of full inlined RL styles would slightly reduce performance on repeat requests, where the RL response would normally be cached in the client.
Possible issue: Cache invalidation for CSS
A simple implementation without ESI or similar would couple the cache life time of HTML and CSS. In many cases this is useful, as there is naturally a strong coupling between the two, but there might be dynamic features or general changes that we would prefer to apply more consistently and quickly.
For example, a font change can currently be applied fairly quickly in a way that applies to both old & new cached HTML. In a simple implementation without ESI, the font change would instead take up to a month to apply to all pages, as the lifetime of the CSS is coupled to the HTML.