angular / angular-cli

CLI tool for Angular
https://cli.angular.io
MIT License
26.73k stars 11.98k forks source link

[RFC] Eliminate Render Blocking Requests #18730

Closed alan-agius4 closed 3 years ago

alan-agius4 commented 4 years ago

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

Summary

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

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:

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

kevinfarrugia commented 4 years ago

Small note: I think using a faux media type for async loading CSS will download the CSS file anyway on some browsers.

Read more:

https://bugs.chromium.org/p/chromium/issues/detail?id=977573

https://www.filamentgroup.com/lab/load-css-simpler/

fabianrios commented 4 years ago

does this also means in dev. mode CSS could be reloaded and injected with out the need to reload the application? because that would be a great boost.

alan-agius4 commented 4 years ago

does this also means in dev. mode CSS could be reloaded and injected with out the need to reload the application? because that would be a great boost.

@fabianrios, this is already possible when using HMR.

wilmarques commented 4 years ago

Should these features be on opt-in or opt-out bases?

I would vote for opt-out.

If this would compromise the dev experience, maybe a clear option on the new schematic. So one can choose appropriately.

fabianrios commented 4 years ago

@fabianrios, this is already possible when using HMR.

@alan-agius4 not completely, it happens for the global CSS files but the ones inside the components don't get injected with out reloading.

alan-agius4 commented 4 years ago

@fabianrios, this is already possible when using HMR.

@alan-agius4 not completely, it happens for the global CSS files but the ones inside the components don't get injected with out reloading.

You need to configure your application to handle components changes: https://github.com/angular/angular-cli/issues/17324#issuecomment-686440970

Let’s however keep on the RFC topic please.

adnanebrahimi commented 4 years ago

Remove unused CSS https://web.dev/unused-css-rules/

fabianrios commented 4 years ago

@alan-agius4 that answer was very very helpful, sorry to deviate the topic.

elliotleelewis commented 4 years ago

Agree with @adnanebrahimi. The easiest way to remove unused CSS in Angular apps is to use @angular-builders/custom-webpack with the purgecss-webpack-plugin add-on (which also isn't a perfect solution, as it breaks cache-busting hashes), which is way too complicated as it requires knowledge of Angular Builders, Webpack, and PurgeCSS.

For such a huge savings in file-size, its really strange that something similar isn't supported out of the box by Angular.

aaronfrost commented 4 years ago

In order to understand what is being proposed, it is important to define "Critical CSS". I haven't done testing, but I would guess that critters and critical have different methods of defining the Critical CSS that they inline. I would imagine the same thing for Penthouse. Further, based on my understanding of how it is calculated would mean that each route in the app could have distinct Critical CSS.

alan-agius4 commented 4 years ago

Hi all, thanks for the excellent feedback.

Agree with @adnanebrahimi. The easiest way to remove unused CSS in Angular apps is to use @angular-builders/custom-webpack with the purgecss-webpack-plugin add-on (which also isn't a perfect solution, as it breaks cache-busting hashes), which is way too complicated as it requires knowledge of Angular Builders, Webpack, and PurgeCSS.

For such a huge savings in file-size, its really strange that something similar isn't supported out of the box by Angular.

@adnanebrahimi & @elliotleelewis, unused CSS removal is a very interesting topic and is on the radar. However, it gets tricky if your application content is dynamic where the parts of the content gets retrieved from an API/Database or classes are interpolated example: class-{{category}}. as in such cases, used CSS will removed. So it's a double edged sword.

While unused CSS is definitely useful, it doesn't help reducing or removing render blocking requests. It's important to remember that a 0Kb CSS file still impacts performance negatively, because of the time spent for a connection to be estimated and the file to be downloaded all while rendering is blocked.


In order to understand what is being proposed, it is important to define "Critical CSS". I haven't done testing, but I would guess that critters and critical have different methods of defining the Critical CSS that they inline. I would imagine the same thing for Penthouse. Further, based on my understanding of how it is calculated would mean that each route in the app could have distinct Critical CSS.

  • What is meant by Critical CSS?
  • Can the Critical CSS of an app change per route? If I have 20 statically analyzable routes in my Angular app, can I have two potential Critical CSS definitions? If yes, then generating the Critical CSS gets significantly more complex and time consuming.

@aaronfrost, indeed that each tools has a different methods of detect Critical CSS. In Critters terms, it's the entire document structure that is rendered. From their readme

It's a little different from other options, because it doesn't use a headless browser to render content. This tradeoff allows Critters to be very fast and lightweight. It also means Critters inlines all CSS rules used by your document, rather than only those needed for above-the-fold content.

When it comes to Critical CSS per route, this is what's being proposed for SSR and pre-rendering. For SSR, critical CSS will be extracted and inlined during runtime (Given a more lightweight and non Webpack plugin of Critters is available.), while for pre-rendering this shouldn't impact much the build time due to the parallel nature of the build.


Small note: I think using a faux media type for async loading CSS will download the CSS file anyway on some browsers.

Read more:

https://bugs.chromium.org/p/chromium/issues/detail?id=977573

https://www.filamentgroup.com/lab/load-css-simpler/

@kevinfarrugia, I think you meant that the CSS file will not be downloaded when using an invalid media. While the article from loadCss suggests not to use an invalid media attribute. it's actually what they use internally: https://github.com/filamentgroup/loadCSS/blob/c14df53ebed55d4d06490d19bbc0265e2af19b98/src/loadCSS.js#L35.

That being said, It does look like using media="print" will have the same effect as media="only x".

aaronfrost commented 4 years ago

The Scully team is going to build a post-render plugin that does this for those who care. We will provide it as a built in plugin so that you don't have to add it separately to your package.json.

Puppeteer has this info ALREADY available for each page. So we can essentially get this info for free.

Soon Scully is going to add Universal pre-rendering as well. We will be interested to see what is decided here to see how we can add that support into Scully-universal as well.

ValentinFunk commented 4 years ago

We're using penthouse/critical to generate critical CSS for each route, then inject this CSS in the universal server at the moment. It's quite slow and sometimes just doesn't work.

One really performant solution I tried is to include the critical generation into the app itself (i.e. the script that penthouse injects to find selectors that apply) and send the generated page back to puppeteer, then navigate to a new route from within the same app. Essentially this prevents having to load the whole app for each page you want to render critical CSS for. Unfortunately it doesn't work since the nghost attributes are different on client and server. This one was easy to fix, but they are also different between page loads. If you could give components a unique ID at AOT compile that would make this approach possible.

alan-agius4 commented 4 years ago

@Kamshak, that was one of the reasons why we are proposing to use Critters which doesn't use Puppeteer or similar headless browsers solutions.

In general App-Shell and Universal serve different purposes and typically when using Universal the use-case where you want to use an App-Shell is together with Service Workers. This is because the Universal response will always override the contents of the app-root in an App-Shell.

felipeplets commented 4 years ago

This would be a great addition to Angular CLI.

Open Questions

  • Should these features be on opt-in or opt-out bases?

I vote for opt-out.

  • Should we add the bundle budgets to existing applications? If yes, what should be the default threshold for warnings and errors.

yes. Is there any metric collected by Angular CLI Analytics that can help to make a more informed decision about threshold?

  • Should we add the bundle budgets to new applications? If yes, what should be the default threshold for warnings and errors.

yes

BenRacicot commented 4 years ago

A vote for opt-out here. If the feature is added we can choose to adopt it, or not :)

I wrote about relying on global (1 level specific) styles for all elements within your application. This keeps component styles small and also at single levels of specificity for easy override... and more.

If the CSS blocks I mention are broken out to load as their own files and only strategically loaded on pages which require them then CSS weight and FOUC could be even further optimized and component weight reduced also.

<link rel="stylesheet" href="global-layout.css"> <-- minimum CSS first

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

Consider a prefetching strategy for broken up global styles

Also, combining the proposed noscript with prefetching to cache the rest of the blocks would remove CSS loading time from UX.

<link rel="prefetch profile style" title="profile styles" href="profile.css">
ruslanguns commented 4 years ago

I think It should be an opt-out option to ease the way for those who are starting with the framework and who do not understand the subject very well since this functionality if it comes by default, would speed up the app built time and solve this problem in bundle sizes and who have more knowledge and who need to disable this option for some reason, allow an option to disable this behavior would be fine.

I think that considering a threshold on each specific project should be analyzed and based on the average of the applications that are usually made with Angular, to say a measure of this threshold would be totally unwise, so I agree with @felipeplets that this information who's better than the analytics collected by Angular and Google itself to make this decision. But yes, I think bundle budgets should be added to both new and existing applications. Come on, this is highly cool.

alan-agius4 commented 3 years ago

This concludes the 2 week commenting period for our RFC. I'd like to thank everyone for their time reading through this and providing valuable feedback. We at the Angular team really appreciate this.

To wrap this up, the main takeaways are:

  1. Based on the feedback above, all the mentioned features should be on opt-out bases.
  2. It was noted that we should not used a faux media only x, instead we should use print.
  3. New budgets should be added for both new and existing projects.
  4. The community requested an out of the box solution to prune unused CSS.