Next.js: [Font Optimization] Asynchronous non-critical font styles

Created on 11 Aug 2020  路  5Comments  路  Source: vercel/next.js

Feature request

Whilst I hugely appreciate @prateekbh's work to enhance the external font loading experience, it would be nice to support (or maybe even make default) the option to treat these font styles as non-critical.

RE: https://github.com/vercel/next.js/pull/14746, https://github.com/vercel/next.js/pull/16031

Describe the solution you'd like

An async option could be defined in the experimental config (but I would argue it might be best to assume that font styles are non-critical and set this to true by default) which would extract the external CSS into a separate CSS file.

This CSS file would be referenced in the head, and set to media="print" on initial load then to media="all" on app hydration. With lots of modern CSS-in-JS libraries extracting to critical CSS, this would avoid clashing opinions on what should actually by considered crticial.

Describe alternatives you've considered

I built and maintain next-google-fonts but this doesn't extract CSS at compile time; it just asynchronously loads Google Fonts.

Additional context

Again, thanks so much for your work on this @prateekbh. Happy to help out where I can

Most helpful comment

Well choosing display=swap makes your text appear faster but definitely has a problem of FOUT.
I will soon look into the effects of display:optional + adding a preload with this optimization. This option adds a 100ms wait to FCP and can render the text in actual font without FOUT.

I would suggest people to opt for the async css when they are very sure that fonts are of least interest but not something to come out of the box, given that if you're adding a font in the first place you might want it to load faster as well than later.

All 5 comments

@joe-bell thanks for the feedback, highly appreciate the interest. I did investigate the approach you mentioned. However there are some pitfalls to the same.

  • The default priority for browser to fetch font definitions and fonts is HIGHEST. It needs this info to paint the page quickly when the font-display is blocking and also helps in displaying the correct font ASAP with font-display optional. Both of these impact the core web vitals. So with the current approach the priorities remain effectively unchanged.
  • I have not tested this but the approach of async stylesheet might harm font-icons. With print=media the stylesheet is fetched at the LOWEST priority and thus the user will see the text instead of the icon for a very long time.

Let me know if there are alternate findings

@prateekbh thanks for the detailed feedback here, it's helpful to understand how this plays apart in the Core Web Vitals work!

More and more people are choosing to use font-display: swap; as fonts are not considered critical against layout styles. It's even the default option in Google Fonts now. Having an option to render the fonts asynchronously鈥搒o they are not blocking other styles鈥搘ould bring huge perf gains and avoid dreaded FOUT.
I'm not sure if it's worth worrying about font icons any more, now that SVG is considered to be the best approach.

I would really recommend taking a look at this recent article by @csswizardry; "The Fastest Google Fonts". It does a good job of explaining how important this feature would be

Thanks for the links @joe-bell. I'll address the concerns in the following points

  1. I left font display: swap intentionally as the effect on CLS from display: swap and perf gains from both approaches would be effectively same. Either early(via current approach) or late(via async css) there would be some layout shift so the net effect on web vitals are same.
  1. > SVG is considered to be the best approach.

I completely agree with this and don't wanna encourage otherwise. I just mentioned it for completeness sake.

  1. The Async CSS
    There are three main concerns that I have
  2. Delayed display
    The Async CSS approach is intentionally delaying the loading of the fonts by using media=print. This is unintuitive for most people coming from any other web development background. If developers host CSS for fonts or download the font and refer them in their CSS they would still see the font declarations arriving along with the CSS and not after.
    While using the components like next-google-fonts or any other specialized ones, the users make an intentional choice of delaying the fonts and know that fonts are not one of the important part of the product. _Where as making it a default forces this call on them_. Based on product requirements a font might still be an important part of branding e.g. (I am taking a guess here) see google photos, medium, airbnb etc.
  • Extensibility
    While display: swap renders the text ASAP it may still have CLS issue when a font of a different height is loaded on the page. Our team is currently investigating if this can be mitigated in some way. By inling the fonts declarations the users have a choice to switch to display: blocking(if fonts are utmost importance) or display optional(something we're looking into to eliminate CLS on pages).
    So the way I look at this is that impact radius of improvement is slightly bigger with the current approach. These options go away when we make lazy loading of any sort as the default. With Async CSS the display: blocking would be a terrible UX and display: optional would also have FOUT.

  • Minimally Invasive
    There might be some repetition from point 1 but the current approach the idea of inlining the fonts css got us to eliminate the render blocking aspect without changing the DX or the impact on the loading waterfall. That said, we still have a step left of having a threshold to inling the font declarations in order to prevent the markup become huge. The strategy of Async CSS can definitely come in handy for loading the CSS beyond such a threshold.

Note: I agree that there might be a use case where someone would want to use async css to load fonts and I would still recommend component like next-google-fonts for the use case.

@prateekbh gotcha, thanks for taking the time to writing this up

I think I agree in this case that making this the _default_ option is not a good idea. However, what approach would you suggest to make sure people who are using font-display: swap can avoid FOUT due to render-blocking CSS?

Do you still think it's viable to have this async feature available for those using font-display: swap (or maybe even automatically if they pass the Google Fonts parameter), or would you see this as out-of-scope for the current experimental implementation?

Well choosing display=swap makes your text appear faster but definitely has a problem of FOUT.
I will soon look into the effects of display:optional + adding a preload with this optimization. This option adds a 100ms wait to FCP and can render the text in actual font without FOUT.

I would suggest people to opt for the async css when they are very sure that fonts are of least interest but not something to come out of the box, given that if you're adding a font in the first place you might want it to load faster as well than later.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Knaackee picture Knaackee  路  122Comments

nickredmark picture nickredmark  路  60Comments

dunika picture dunika  路  58Comments

robinvdvleuten picture robinvdvleuten  路  74Comments

nvartolomei picture nvartolomei  路  78Comments