Skip to content

[RFC] Eliminate Render Blocking Requests #18730

Closed
@alan-agius4

Description

@alan-agius4

Authors: Alan Agius (@alan-agius4)
Status: Closed
Closing Date: 2020-09-22

Summary

We’re proposing to eliminate render blocking requests loading by:

  • Loading CSS files asynchronously in a single-page application.
  • Inline critical CSS in Angular applications which use Angular Universal Server Side Rendering Pre-rendering, App Shell and Angular Client Side Rendered Applications.
  • Inline Google Fonts and Icons.

At the moment there is no easy or streamlined way to identify, extract or inline critical CSS in Angular Universal applications. We’re proposing to offer an out-of-the-box solution with little or no configuration needed.

Motivation

CSS files are render-blocking because the browser must download and parse these files before starting to render the page. This makes CSS files a bottleneck when they are large or when having poor or limited network connectivity. Each of these files will result in a penalty on the Performance Score of your application #17966.

We can reduce this render-blocking time and at the same time improve the first contentful paint (FCP) by extracting and inlining the critical CSS and loading the CSS files asynchronously.

Proposal

Load CSS files asynchronously

In most cases JavaScript bundles will take a longer time to download, parse than for Angular to bootstrap and start rendering the first component thus the chance of having Flash of unstyled content (FOUC) is relatively low.

We are proposing to introduce an experimental async CSS loading use the “media” technique which can be opted-in/out via an option angular.json

Before

<link rel="stylesheet" href="styles.css">

After

<link rel="stylesheet" href="styles.css" media="only x" onload="this.media='all'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

CSS files budgets

CSS files are great for code-sharing, but other than that they are a bottleneck to achieving great performance. Two of the main reasons for this is that they are render blocking and might contain dead-rules.

Since a CSS file is not strictly associated with the components loaded on the page, a number of CSS declarations that get downloaded and parsed will remain unused by the view that was rendered.

Having render blocking and/or dead-rules will cause performance score penalties in Lighthouse.

We propose to add two new bundle budgets allStyle and anyStyle:

  • anyStyle: any given external CSS files
  • allStyle: cumulative size of all external CSS files.

Inline Google Fonts and Icons

We are proposing to introduce an experimental optimization for fonts which can be opted-in/out via an option in angular.json.

During build time we will parse the index.html, download the content of stylesheets originating from https://fonts.googleapis.com/… and inline their content.

This eliminates the extra round trip that the browser has to make to fetch the font declarations, which improves LCP, reduces FOUC, and also unlike other approaches doesn't prohibit Angular applications from taking full advantage of font-display: optional.

Before

<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">

After

<style>
  @font-face {
    font-family: 'Material Icons';
    font-style: normal;
    font-weight: 400;
    src: url(https://fonts.gstatic.com/s/materialicons/v55/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
  }

  .material-icons {
    font-family: 'Material Icons';
    font-weight: normal;
    font-style: normal;
    font-size: 24px;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    display: inline-block;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
  }
</style>

In case the application needs to support Internet Explorer 11 which will be determined via the browserslist configuration, the woff1 definition of the font will also be inlined.

Extract Critical CSS

The most generic which requires zero to no configuration from the developer is to inline critical CSS as a post-rendering step using existing projects such as: penthouse, critters and critical.

The above mentioned tools take different approaches to extract critical CSS. The approach that Critters uses is the best fit for our use cases. The main reason for this is that Critters doesn’t use a headless browser but uses a JavaScript DOM (JSDOM) to render the page content which makes it faster compared to the other tools and hence it makes it valuable to be used as a build time and runtime option. The main trade-off of this is that it doesn't predict the viewport and inlines all the CSS declarations used by the document.

SSR

In Angular Universal SSR we cannot use Critters directly because this is a Webpack plugin and rendering of Angular Universal pages for both dynamic and static applications happens outside of the Webpack build. Therefore, a more decoupled version of Critters core functionality would be needed.

We’ll run Critters during runtime. When a request hits the server and the Angular SSR page is rendered, we will run Critters as a post-rendering phase to extract the critical CSS and inline in the final HTML response.

App-Shell & Pre-rendering

As a post-rendering phase during build time, we’ll run Critters to extract the critical CSS of the rendered page and inline the contents in the HTML document.

CSR

In Angular CSR, we cannot extract and inline CSS because we are unable to run it in a Node.Js environment, However, it is common for CSP’s to have a custom loading experience outside of Angular context defined in the index.html file. Therefore, we are proposing to extract and inline CSS for this use case to reduce the risk of FOUC even further.

Alternatives

Below are some alternatives that we have considered but deemed less useful / less feasible compared to the main proposal.

Annotating critical CSS

Annotating critical CSS with a comment and tools such as postcss-critical-split will extract these into a separate file which can later be inlined.

/* critical:start */
header {
  background-color: #1d1d1d;
  font-size: 2em;
}

.aside {
  text-decoration: underline;
}
/* critical:end */

The drawback of this is that It will be up to the developer to determine which CSS declarations are critical or not. Developers will also not be able to annotate critical styles which are not part of the application such as when depending on a vendored UI framework library such as Material, Bootstrap etc...

Hence this approach is more complex, has a bigger learning curve and is error prone.

Using headless browsers based extractors

Penthouse is a critical CSS extractor and can do so for non SSR’d applications. This is because under the hood it uses puppeteer to generate the critical CSS.

The main drawback of this is that this approach will be different from what’s proposed for Angular Universal and is slower.

Include CSS files in app root component

Another approach would be to include the global styles in the app root component.

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: [
   './styles.css',
   './app.component.css',
 ],
})
export class AppComponent { }

The main drawback of this is that the entire contents of styles.css will be inlined in the HTML page when using Angular Universal or App shell.

DNS-Prefetch and Preconnect Hints

Add dns-prefetch and preconnect hints for for https://fonts.gstatic.com to initiate DNS resolution and a connection.

Additional Resources

Open Questions

  • Should these features be on opt-in or opt-out bases?
  • Should we add the bundle budgets to existing applications? If yes, what should be the default threshold for warnings and errors.
  • Should we add the bundle budgets to new applications? If yes, what should be the default threshold for warnings and errors.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions