Improving Largest Contentful Paint across the JavaScript ecosystem.
As part of project Aurora, Google has been working with popular web frameworks to ensure they perform well according to Core Web Vitals. Angular and Next.js have already landed font inlining, which is explained in the first part of this article. The second optimization we will cover is critical CSS inlining which is now enabled by default in Angular CLI and has a work in progress implementation in Nuxt.js.
Font inlining
After analyzing hundreds of applications, the Aurora team found that developers often include fonts
in their applications by referencing them in the <head>
element of index.html
. Here's an example
of how this would look like when including Material Icons:
<!doctype html>
<html lang="en">
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
...
</html>
Even though this pattern is completely valid and functional, it blocks the application's rendering and introduces an extra request. To better understand what is going on, take a look at the source code of the stylesheet referenced in the HTML above:
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/font.woff2) format('woff2');
}
.material-icons {
/*...*/
}
Notice how the font-face
definition references an external file hosted on fonts.gstatic.com
.
When loading the application, the browser first has to download the original stylesheet referenced
in the head.
Next, the browser downloads the woff2
file, then finally, it's able to proceed with rendering the
application.
An opportunity for optimization is to download the initial stylesheet at build time and inline it in
index.html
. This skips an entire round trip to the CDN at runtime, reducing the blocking time.
When building the application, a request is sent to the CDN, this fetches the stylesheet and inlines
it in the HTML file, adding a <link rel=preconnect>
to the domain. Applying this technique, we'd
get the following result:
<!doctype html>
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin >
<style type="text/css">
@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/font.woff2) format('woff2');}.material-icons{/*...*/}</style>
...
</html>
Font inlining is now available in Next.js and Angular
When framework developers implement optimization in the underlying tooling, they make it easier for existing and new applications to enable it, bringing the improvement to the entire ecosystem.
This improvement is enabled by default from Next.js v10.2 and Angular v11. Both have support for inlining Google and Adobe fonts. Angular is expecting to introduce the latter in v12.2.
You can find the implementation of font inlining in Next.js on GitHub, and check out the video explaining this optimization in the context of Angular.
Inlining critical CSS
Another enhancement involves improving the First Contentful Paint (FCP) and Largest Contentful Paint (LCP) metrics by inlining critical CSS. The critical CSS of a page includes all of the styles used at its initial rendering. To learn more about the topic, check out Defer non-critical CSS.
We observed that many applications are loading styles synchronously, which blocks application
rendering. A quick fix is to load the styles asynchronously. Rather than loading the scripts with
media="all"
, set the value of the media
attribute to print
, and once the loading completes,
replace the attribute value to all
:
<link rel="stylesheet" href="..." media="print" onload="this.media='all'">
This practice, however, can cause flickering of unstyled content.
The video above shows the rendering of a page, which loads its styles asynchronously. The flicker
happens because the browser first starts downloading the styles, then renders the HTML which
follows. Once the browser downloads the styles, it triggers the onload
event of the link element,
updating the media
attribute to all
, and applies the styles to the DOM.
During the time between rendering the HTML and applying the styles the page is partially unstyled. When the browser uses the styles, we see flickering, which is a bad user experience and results in regressions in Cumulative Layout Shift (CLS).
Critical CSS inlining, along with asynchronous style loading, can improve the loading behavior. The critters tool finds which styles are used on the page, by looking at the selectors in a stylesheet and matching them against the HTML. When it finds a match, it considers the corresponding styles as part of the critical CSS, and inlines them.
Let's look at an example:
<head> <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'"> </head> <body> <section> <button class="primary"></button> </section> </body>
/* styles.css */ section button.primary { /* ... */ } .list { /* ... */ }
In the example above, critters will read and parse the content of styles.css
, after that it
matches the two selectors against the HTML and discovers that we use section button.primary
.
Finally critters will inline the corresponding styles in the <head>
of the page resulting in:
<head> <link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'"> <style> section button.primary { /* ... */ } </style> </head> <body> <section> <button class="primary"></button> </section> </body>
After inlining the critical CSS in the HTML you will find that the flickering of the page is gone:
Critical CSS inlining is now available in Angular and enabled by default in v12. If you're on v11,
turn it on by setting the inlineCritical
property to true
in angular.json
. To
opt into this feature in Next.js add experimental: { optimizeCss: true }
to your next.config.js
.
Conclusions
In this post we touched on some of the collaboration between Chrome and web frameworks. If you're a framework author and recognize some of the problems we tackled in your technology, we hope our findings inspire you to apply similar performance optimizations.
Find out more about the improvements. You can find a comprehensive list of the optimization work we've been doing for Core Web Vitals in the post Introducing Aurora.