angular-cli: [RFC] Eliminate Render Blocking Requests
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
- https://web.dev/vitals/
- https://web.dev/extract-critical-css/
- https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch/
- https://github.com/GoogleChromeLabs/critters/
- https://web.dev/preload-optional-fonts/
- https://web.dev/render-blocking-resources/
- https://web.dev/first-contentful-paint/
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.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 91
- Comments: 18
Remove unused CSS https://web.dev/unused-css-rules/
Hi all, thanks for the excellent feedback.
@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.
@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
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.
@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 asmedia="only x"
.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.
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.This would be a great addition to Angular CLI.
I vote for opt-out.
yes. Is there any metric collected by Angular CLI Analytics that can help to make a more informed decision about threshold?
yes
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.
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.
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.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 that answer was very very helpful, sorry to deviate the topic.
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.
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/