w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-fonts] Add a `font-display` keyword to eliminate `@font-face` FOIT & layout shifts #7271

Open xiaochengh opened 2 years ago

xiaochengh commented 2 years ago

Currently, we don't have any easy & sound way to eliminate FOIT & layout shifts caused by web fonts:

I'm proposing adding a new keyword to the font-display descriptor, tentative named font-display: critical, which:

In this way, as long as the @font-face is defined in a render-blocking style sheet (which everyone knows about but hadn't been specified until recently), then it will block the first render of the document, and hence eliminate FOIT / layout shifts.

This feature is supposed to be used on web fonts that are truly critical, so that developers want to eliminate FOIT / CLS at a great cost of delaying rendering. It will be footgun-ish and shouldn't be used arbitrarily.

Use cases

Basic usage:

<head>
  <style>
  @font-face {
    font-family: my-font;
    font-display: critical;
    src: url(/my-font.ttf);
  }
  body { font-family: my-font; }
  </style>
</head>
<body>
  Text in my-font with absolutely no FOIT / CLS
</body>

There's also a particular use case I'd like to support: making a 3rd party web font render-blocking without knowing the font url.

Developer page:

<head>
  <link rel=stylesheet href="https://3p-fonts.com/cool-font.css?critical=yes">
  <style>body { font-family: cool-font; }</style>
</head>
<body>
  Text in cool-font with absolutely no FOIT / CLS
</body>

https://3p-fonts.com/cool-font.css?critical=yes:

@font-face {
  font-family: cool-font;
  font-display: critical;
  /* url is maintained by 3p-fonts.com. May change at any time; may even be generated. */
  /* Developers shouldn't preload this url or inline this style sheet. */
  src: url(/some-random-hash-123654ABCFED-or-whatever.ttf);
}

Possible discussions

Possible blockers

@tabatkins @chrishtr

jfkthame commented 2 years ago

This feature is supposed to be used on web fonts that are truly critical

I'm wondering exactly what it means for a web font to be "truly critical", such that it's reasonable for them to block rendering indefinitely; and if a web font is so critical to the site, why would they be loading it from a third-party server (which might be down, inaccessible, blocked, hacked, etc. at any given time)?

Lorp commented 2 years ago

Examples of truly critical webfonts:

jfkthame commented 2 years ago

Could such cases be handled by directly embedding the font as a data URL within the stylesheet?

Lorp commented 2 years ago

Yes that’s one way, and you could embed the stylesheet in the document of course. In normal practice AFAIK, data URL font stylesheets are typically separate from the main CSS files, so then you might need to defend against the main CSS loading but the font CSS not…

xiaochengh commented 2 years ago

if a web font is so critical to the site, why would they be loading it from a third-party server (which might be down, inaccessible, blocked, hacked, etc. at any given time)?

I don't think this is a new issue. The web font is usually served from the same (or related) server as the font stylesheet, which is also render-blocking and third party.

Could such cases be handled by directly embedding the font as a data URL within the stylesheet?

Data url doesn't guarantee no FOIT / layout shift. For example, in Blink, data urls are handled the same was as all other urls:

  1. Run style & layout to see if the font is really used. This step may already result in a paint of the page with fallback
  2. Asynchronously decode the data url and then the font
  3. Add the font into available fonts and then re-render the page, causing issues
css-meeting-bot commented 2 years ago

The CSS Working Group just discussed [css-fonts] Add a font-display keyword to eliminate @font-face FOIT & layout shifts.

The full IRC log of that discussion <dbaron> Topic: [css-fonts] Add a font-display keyword to eliminate @font-face FOIT & layout shifts
<Rossen_> s/Resolution:/RESOLVED:/
<dbaron> github: https://github.com/w3c/csswg-drafts/issues/7271
<dbaron> xiaochengh: I propose adding a new keyword to font-display descriptior so it's critical and blocks load event of stylesheet.
<dbaron> xiaochengh: The purpose is to eliminate flash of invisible/unstyled text or layout shift caused by web fonts.
<dbaron> xiaochengh: There are many ways to mitigate flash or layout shift, but either complicated to use or have other issues.
<dbaron> xiaochengh: This one keyword proposal can hopefully solve this.
<dbaron> Rossen: I see a conversation back and forth between Jonathan and Lawrence on the issue. Jonathan is raising some points -- not sure they've been answered, especially his first point about what is truly critical.
<chrishtr> q+
<dbaron> s/Lawrence/Laurence/
<dbaron> s/descriptior/descriptor/
<Rossen_> q?
<Rossen_> ack chrishtr
<dbaron> chrishtr: regarding critical -- there are examples in issue of font use cases that would be considered critical. This isn't a question about why they'd be served from a 3d party domain if they were critical. If you want your site to be fast you'd ideally serve from same domain, but many sites use 3d party f ont service to serve fonts, which allows 3d party font service to update fonts over time.
<dbaron> chrishtr: xiaochengh is proposing that critical fonts should be loaded only if they're truly critical. Hope that font providers could have query parameter so that their stylesheet could say it's render blocking.
<dbaron> Rossen_: Jonathan...?
<dbaron> jfkthame: Don't have anything specific to add... was trying to understand goals and how it would work. I have a little reluctance to complicate font-display ... already fairly complex that few people understand. But I haven't tried to think through implementation issues.
<dbaron> jfkthame: ... of the new critical value.
<dbaron> jfkthame: Other question is how this relates to font loading api. Are these use cases where authors should be using font loading api to more closely cnotrol what is happening? Not sure...
<Rossen_> ack fantasai
<xiaochengh> q+
<astearns> q+
<dbaron> fantasai: My concern is that it prevents the page from showing text. I understand thatintent is that authors not use it for most of the text, but I think authors might use it for that. We normally don't make it easier to prevent the page from loading for long periods of time, perhaps forever.
<Rossen_> ack xiaochengh
<dbaron> xiaochengh: problem is developers are already trying to block render of page with various hacks. One example is to add empty external stylesheet that blocks rendering so font can load. Other is add display:none or visibility:hidden and remove when font is loaded. Since developers are already trying to do it, we can provide a better way.
<chrishtr> also, style sheets and scripts already block rendering, potentially indefintely.
<chrishtr> this attribute also indicates intent directly, and allows the UA to ignore it after a timeout
<dbaron> fantasai: I think technique of using visibility:Hidden and then visible is the developer very clearly saying that it's not to be rendered until font loads. Very clear they want invisible.
<emilio> q+
<dbaron> xiaochengh: not just making element invisible... making entire page invisible.
<dbaron> Rossen_: This is use-case specific. People can do it in a more targeted way.
<Rossen_> ack astearns
<plinss> q+
<fantasai> fantasai: my concern is that someone will be like "Oh, my wedding invitation *has* to be loaded in this font because it looks ugly otherwise, so I definitely consider this a critical font" and then the reader of the invitation, on a different connection, ends up never able to read the page
<dbaron> astearns: I think I agree with fantasai that the current hacks might be sufficient for this. But I got on queue because of concern with how this works only with style-blocking style sheets. I worry about adding something that will work in some cases but not in others. We'd have to specify what this does, if anything, if the style sheet is not render blocking. And I'm worried about adding something that has that works and doesn't work
<dbaron> astearns: characteristic.
<dbaron> xiaochengh: The intention is to make it render-blocking but after the document has already started there's no way to block render, so to keep things simple I'm just making it block load of style sheet.
<astearns> s/style-blocking/render-blocking/
<Rossen_> ack emilio
<dbaron> emilio: Similar to that... this would imply the font should load unconditionally and fully -- don't have unicode-range (presumably ignored) -- my other question isn't this more similar to how background images block the load event of the page but not of the style sheet. Doesn't achieve the rendering blocking that you want... but maybe it does? Background image loads get started before layout rather than during layout like fonts.
<chrishtr> q+
<Rossen_> ack plinss
<dbaron> Peter: I agree with fantasai. I hear the argument that authors do this so we should make it easy. That argument falls down when authors are doing something bad -- we have no obligation to do that. We should teach authors how to do fallbacks, progressive rendering ,etc.
<fantasai> +1 to "don't optimize for making the bad things easy"
<dbaron> chrishtr: Peter, I don't think there is a good way to do it right now. Only way to do it causes flashing on load. size-adjust was added but ??. This is a clean solution to this natural problem.
<dbaron> Peter: We have to weight harms between flashing and blank content. Look for other alternative rather than blocking?
<dbaron> Rossen: Not sure how this isn't creating flashing as well while you're waiting.
<dbaron> chrishtr: Shows white.. shouldn't consider that a flash of unstyled content.
<dbaron> fantasai: We already have the block keyword that shows white.
<Rossen_> q
<Rossen_> ack chrishtr
<dbaron> chrishtr: The existing keyword shows white only for the text, not the page
<dbaron> ?: ... and there's layout shift.
<dbaron> Peter: I'd say both preferable to blank page.
<dbaron> chrishtr: I think use cases for either.
<dbaron> plinss: Authors don't always consider all the factors
<dbaron> s/Peter/plinss/g
<dbaron> plinss: Doesn't mean we should make it easier... think it will lead to more abuse.
<dbaron> Rossen_: I'm hearing a good bit of feedback. So I think we should take this back to the issue and accumulate a little more consensus there before we bring it back for a resolution. Also given how many people missing today.
<fantasai> If we're adding a keyword for this, it needs to be something really obnoxious and obvious, like "block-entire-page-load-forever"
<dbaron> Rossen_: Anything else you wanted to highlight today, xiaochengh?
<dbaron> xiaochengh: The intention is not to help authors do bad things... let me outline this another way. We're trying to help authors make a tradeoff more easily... tradeoff between page stability and responsiveness. No easy way to go to one end of the tradeoff.
<dbaron> Rossen_: We'd like to move it back to the issues.
<astearns> I don’t think this is necessarily a bad thing, but I am not convinced there is no current way to achieve an appropriate result
<dbaron> fantasai: My understanding is that the proposal is to add a keyword that blocks the page rendering *literally forever* if the font doesn't load. If it's still not loading 30 seconds later because the font isn't loading, that's a problem.
<fantasai> s/problem/problem for the user/
<dbaron> plinss: I think the concern is that authors will unknowingly use this badly and wind up doing bad things by accident.
<tantek> +1 fantasai, plinss this makes it too easy for authors to do *harmful* things to users
<florian> +1 to not linking this, for the reasons said by fantasai and plinss
<emilio> one could make a case that the same can effectively already happen for any stylesheet tho
<chrishtr> Fantasai: this is not new, style sheets can and do already block rendering. I do think the spec for this should say the UA should provide a timeout.
<florian> s/linking/liking/
astearns commented 2 years ago

On the question of indefinite blocking in the minutes above, I was interpreting this

* It has the same [Font Display Timeline](https://www.w3.org/TR/css-fonts-4/#font-display-timeline) as `font-display: block`

to mean that the load block would only apply for a short time, then be lifted (like how font-display:block will still use a fallback font after a short period elapses). Is that correct?

xiaochengh commented 2 years ago

The load block will apply until the font is loaded.

This (setting a font display timeline) is just for technical completeness. For example, if we have a font-display: critical font in a JS-inserted (hence non-blocking) sheet , then it's possible that the UA renders the page while the font is still pending, in which case we still need a font display timeline to decide whether the fallback is visible.

xiaochengh commented 2 years ago

@emilio: Similar to that... this would imply the font should load unconditionally and fully -- don't have unicode-range (presumably ignored) -- my other question isn't this more similar to how background images block the load event of the page but not of the style sheet. Doesn't achieve the rendering blocking that you want... but maybe it does? Background image loads get started before layout rather than during layout like fonts.

It's correct that this implies the font should load unconditionally and fully. It doesn't mean that unicode-range gets ignored -- it still affects which text the font face applies to, similar to the other descriptors for font selection.

The behavior that background images block the load event of the page but not the style sheet seem to be an unspecified behavior. We can't rely on that.

Edit: font-display: critical doesn't void segmentation. We can still segment a font family by unicode range in the same way as before, and then mark the most critical ones (as we predict) as critical. We can make the prediction based on, e.g., page language, text snippet, etc.

As an example, Google Fonts accepts a text= parameter that gives you a segmented font face for a given text snippet:

https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap&text=%E4%B8%80%E4%BA%8C%E4%B8%89

xiaochengh commented 2 years ago

Let me summarize and respond to some major concerns raised in the meeting.

The biggest concern is that since render-blocking may cause the page rendering to be blocked infinitely (which is worse than anything else), we shouldn't add new render-blocking things.

This is the same general concern I got when specifying "render-blocking" in the HTML spec. To resolve that, UA is allowed to unblock rendering if an internal timeout is reached, even if there are still pending render-blocking resources (scripts & style sheets). See https://html.spec.whatwg.org/C/#render-blocked

So this proposal won't make the page never rendered. Nor do other render-blocking resources.


Another point is we shouldn't make it too easy for developers to do potentially bad things (e.g., page never renders). If developers are doing workarounds/hacks to achieve that (like adding/removing display: none with JS), let them be; they must be very explicit when doing potentially bad things.

There might also be concerns that putting things in the declarative syntax is the UA officially saying that this use case is legit and ok to do, whereas the JS hacks don't admit that.

First of all, making fonts render-blocking is not an inherently bad pattern, but an alternative FCP vs. CLS tradeoff that developers want to achieve. Forcing a complicated solution is a double-edged sword that, while preventing misuse of this feature, may also lead to other suboptimal workarounds and confusions.

Besides, existing JS-based workarounds (like removing display:none on FontFace.load) are not really complicated enough to prevent misusing. What is worse, such workarounds do not admit the UA-defined timeout, and would result in a strictly worse UX -- they can actually keep the page blank forever, while font-display: critical won't due to the UA-defined timeout.

Since developers already have a strong need for this, if we ignore it and pretend it's not happening, it eventually comes at the expense of the user because the UA can't do anything about something it doesn't admit. I think it's still better if we can make legit use cases easy to achieve while having a safety net (the UA-defined timeout) against misuses (and bad network), than simply banning it.

chrishtr commented 2 years ago

if an internal timeout is reached

We could also strengthen the language around timeout. e.g. "the user agent should use a more strict timeout for render-blocking fonts, as the user impact of font fallback is less than a completely unstyled document".

astearns commented 2 years ago

Since developers already have a strong need for this

Could you elaborate on why this need is “strong”? I am not sure I have seen evidence for this assertion yet.

xiaochengh commented 2 years ago

Since developers already have a strong need for this

To clarify, "this" means using web fonts without causing layout shifts.

It should be quite evident that developers want to achieve it but there's no easy way yet: https://www.google.com/search?q=web+font+cls

astearns commented 2 years ago

OK, thanks for the clarification. I think things are a little fuzzier about whether developers want to block the entire page rendering until we can guarantee there will be no layout shifts from font loading.

yisibl commented 2 years ago

@xiaochengh Can we render-blocking within a specific element via contain + font-display: critical?

xiaochengh commented 2 years ago

I think things are a little fuzzier about whether developers want to block the entire page rendering until we can guarantee there will be no layout shifts from font loading.

Let me reiterate that it's about tradeoffs. I think the following is a reasonable tradeoff that developers want to achieve:

font-display: critical ensures the first bullet. Together with the UA-defined timeout, the second bullet is also ensured.

I don't know exactly how many developers want to do that (other than this AMP example and @Lorp's previous comment), but given how widely font-display: optional has been recommended as a way to achieve a guaranteed 0 CLS but at a huge cost (page ends up in a fallback font), I think quite a number of developers currently using font-display: optional (possibly in combination with preloads) would actually prefer font-display: critical.

Can we render-blocking within a specific element via contain + font-display: critical?

That sounds like content-visibility?

Lorp commented 2 years ago

Put another way, there are times when (website) users need to know when “critical” fonts are missing. Is it haram that browsers should inform users of such failures?

fantasai commented 2 years ago

@Lorp Sounds like a use case for a missing-symbol generic font to use as the fallback, if it's so critical that you shouldn't be allowed to see the text without the specified font.

@xiaochengh Adding a timeout makes this a lot more palatable. That said I'd want the keyword to be a lot more obvious that it's introducing render blocking on the whole page, maybe something like font-display: block-page, since none of the other font-display keywords have such a global effect.

I also think @yisibl's comment about render-blocking a specific element deserves investigation. For the cases where that's desired, we should make that as easy to do as render-blocking the whole page so that authors are more likely opt to render-block just the one element instead of the whole page when render-blocking the whole page isn't necessary.

xiaochengh commented 2 years ago

I'm fine with renaming it to font-display: block-page, with a note that it falls back to font-display: block if the UA still wants to render a frame while the font is pending (in cases like, e.g., if the timeout has been reached or if the font is in an in-body style sheet).

I think @yisibl's comment might be out of the scope of this issue, since this issue is about reducing FOIT and CLS, which are for the entire page. Maybe it should be discussed in its own issue?

chrishtr commented 2 years ago

Hi, any further thoughts on this issue? I hope we can discuss it again at the next meeting this week, and I think Xiaocheng has addressed all of the concerns, especially with an improved name and advice to UAs to use a (much) shorter timeout than other resources.

litherum commented 2 years ago

I'm generally skeptical of this proposal. In general, it's pretty scary to add more to the web platform that intentionally makes pages load slower.

People do base64 encode font data into data: urls in stylesheets today, presumably to get the kind of behavior proposed in this issue. There are 2 problems with this existing approach:

  1. It's unwieldy for the website maintainer; they require a build step to put the font file into the CSS
  2. Base64 introduces a ~3x size blowup

What could convince me that this proposal was a good idea is evidence of at least one author saying:

  1. We use this technique today for small fonts
  2. We want to use it for big fonts
  3. But we can't because of the size blowup
litherum commented 2 years ago

fonts used for essential UI components, e.g. Google Material Symbols

Icon fonts are an anti-pattern and we should not build new CSS features for them.

jfkthame commented 2 years ago

fonts used for essential UI components, e.g. Google Material Symbols

Icon fonts are an anti-pattern and we should not build new CSS features for them.

In addition, I am still uneasy with the idea that such an icon font should ever block rendering of the page. Surely font-display: block is the more appropriate way to handle them.

Yes, it's not guaranteed to prevent any layout shift. That's the nature of the internet, if a page is dependent on a variety of resources that may load at different speeds (or might occasionally fail to load at all).

Developers still have the option of using the Font Loading API to manage fonts, and could use this to explicitly block rendering until certain resources are ready. So they don't need this proposal in order to achieve that outcome. It may make it easier, but I'm not sure that's desirable. As the original comment said, it will be footgun-ish. We shouldn't be making footguns more readily accessible.

xiaochengh commented 2 years ago

@litherum

I think base64 encoding is a strictly (much) worse solution than this proposal in many aspects:

Also, here is an example where the author wants to make a big font render-blocking.

@jfkthame

To achieve the same purpose (block rendering, then unblock on font load or timeout), it will be much more complicated, and hence error-prone, with the Font Loading API. This is the common problem of polyfills, and the reason why we are speccing new features.

I already showed in my previous comments that developers have a strong (and totally legit) need for using web fonts without causing layout shifts. And in case of a strong developer need, I think we should make it easy and risk-free to use.

astearns commented 2 years ago

I am not convinced by the idea that a Font Loading API solution is inherently error-prone. A scripted solution makes it possible to encode the developer’s exact preferences. For instance, you can choose how long to wait (as requested in the linked example) where we would have to pick a single timeout that might or might not fit a particular purpose.

I would be much happier waiting for a widely-used polyfill that we could take as a much clearer sign for determining what exactly should go in to this feature.

xiaochengh commented 2 years ago

Font Loading API polyfill actually has a worse loading performance, which is something I missed in my previous comment.

For example:

<link rel=stylesheet href="https://3p-fonts.com/cool-font.css">
<script>
document.documentElement.display = 'none';
Promise.race([
  document.fonts.load('20px cool-font'),
  setTimeoutAsPromise(1500), // in case connection is bad
]).then(() => document.documentElement.style.display = '');
</script>
<style>body { font-family: cool-font; }</style>

Then we need to wait until the font stylesheet is fully loaded and parsed to start loading the web font. This can be much slower than my proposal, which can use a preload scanner to start loading the font much earlier.

The polyfill can be improved if we also preload the font in the main document, in which case it will be as fast as my proposal. However, this isn't possible if the font URL is managed by a 3rd party provider, which is a major use case I'd like to support.

chrishtr commented 2 years ago

For instance, you can choose how long to wait (as requested in the linked example) where we would have to pick a single timeout that might or might not fit a particular purpose.

How about we specify the timeout in the declaration as a required field? That would allow a custom timeout, and also make it clear in the style sheet what it's doing.

font-display: block-page 2s

font-display: block-page auto-timeout

heycam commented 2 years ago

The polyfill can be improved if we also preload the font in the main document, in which case it will be as fast as my proposal. However, this isn't possible if the font URL is managed by a 3rd party provider, which is a major use case I'd like to support.

By this I guess you mean that the font URL is not knowable by the page, only the style sheet URL is? And because the style sheet can't be fetched (absent CORS headers), its text can't be inserted into the document dynamically (or the font URLs extracted from inside it).

But I think the polyfill can still make it work, since it can watch for the load event of the third party style sheet:

<script>
function reveal() { document.documentElement.style.display = ""; }
document.documentElement.style.display = "none";
setTimeout(reveal, 500);
</script>
<link href="https://fonts.googleapis.com/css2?family=Smooch" rel="stylesheet" onload="document.fonts.load('20px Smooch').then(reveal);">
<style>body { font: 100px Smooch; }</style>
<p>Here is my text.</p>
chrishtr commented 2 years ago

I'm removing agenda+ for now while we go obtain some more evidence and details about this proposal.

xiaochengh commented 2 years ago

@heycam I think your code snippet has the same loading performance as mine. Both have the following timeline:

main document:                              |-----------------------------|
style sheet loading (by preload scanner):      |-------|
style sheet parsed & inserted into DOM:                       *
font loading (initiated by JS):                                |-------------|
rendering unblocked:                                                          *

But the original proposal can achieve:

main document:                              |-----------------------------|
style sheet loading (by preload scanner):      |-------|
style sheet parsed & inserted into DOM:                       *
font loading (by preload scanner):               |-------------|
rendering unblocked:                                            *

Besides loading performance, I'd also like to avoid using a parser-blocking script to call document.fonts.load(), otherwise the script will be blocked on previous style sheets (which are script-blocking), which means parsing of the document will be paused until all previous sheets are loaded. Using an inline onload event handler on the style sheet seems to fix it, but AFAIK inline event handlers are strongly discouraged and even banned in some cases (reference)