Open xiaochengh opened 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)?
Examples of truly critical webfonts:
Could such cases be handled by directly embedding the font as a data URL within the stylesheet?
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…
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:
The CSS Working Group just discussed [css-fonts] Add a font-display keyword to eliminate @font-face FOIT & layout shifts
.
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?
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.
@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
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.
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".
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.
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
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.
@xiaochengh Can we render-blocking within a specific element via contain
+ font-display: critical
?
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
?
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?
@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.
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?
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.
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:
What could convince me that this proposal was a good idea is evidence of at least one author saying:
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.
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.
@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.
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.
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.
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
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>
I'm removing agenda+ for now while we go obtain some more evidence and details about this proposal.
@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)
Currently, we don't have any easy & sound way to eliminate FOIT & layout shifts caused by web fonts:
font-display: optional
elimintes layout shifts and FOIT, but at the cost that the web font may not be used, which limits its usagefont-display
values do not reduce layout shifts at allsize-adjust
descriptor only reduces the layout shift but can't guarantee 0 CLS, and there might still be a FOIT. It's also complicated to use since we need to find out the correct value.I'm proposing adding a new keyword to the
font-display
descriptor, tentative namedfont-display: critical
, which:@font-face
a critical subresource of the style sheet, and hence makes it block theload
event of the style sheet@font-face
eagerly; other font faces are still loaded lazily when usedfont-display: block
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:
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:
https://3p-fonts.com/cool-font.css?critical=yes:
Possible discussions
blocking=render
to the font preload<link>
to block rendering until the preload finishes?blocking=render
from preload. See more discussions atLink
header that preloads the fontfont-display
keyword?font-display: block
. So it doesn't make much sense to be a standalone descriptor and then interact with the otherfont-display
values. That's also the reason why its font display timeline is the same asfont-display: block
, though it's mainly for spec completeness -- if we ever need to use such a font while it's still pending, it's likely a misuse.Possible blockers
font-display: critical
font faces" without having to fully define what other critical subresources are.@tabatkins @chrishtr