Closed jakearchibald closed 3 years ago
Sigh, yes, that is absolutely the intention of that value. The only reason the spec mentions "extremely small block period" for optional
and fallback
is because implementors complained that a 0s block period technically disallows loading from cache, if the cache is slow enough that the timer can advance before it loads.
Yeah, I think at the time we didn't have a good way to express it via fetch.
Web fonts are showing up as one of the primary causes of layout instability. I'd love to be able to give web devs the advice "apply font-display:optional and this won't be a problem", but today we can't do that.
Any thoughts on next steps here?
I agree that we need this mode. Unfortunately, today use of a local, pre-cached web font incurs a two renders to draw most of the time, because the there is no way to make the font block the rendering entirely for a limited time.
Browsers can add heuristics to anticipate this, but there is an inherent tradeoff about whether the font blocks all of rendering that, for high-performance sites, the site author should be allowed to make.
There must be a way, as it's what we do for regular installed fonts.
For some context, for font-display:optional
, when the web font is already cached, Chrome (80.0.3987.7) in some cases may synchronously resolve the network request during style recalc and use it immediately for rendering. This is what I observed after reloading the test case (https://static-misc.glitch.me/optional-font-load/) for a few times.
This sounds like the desired behavior, and I'd love to see it enforced by the spec.
All right, pushed some new text that should make the intent much clearer:
If the font can be loaded "immediately" (such that it's available to be used for the "first paint" of the text), the font is used.
Otherwise, the font is treated as if its [=block period=] and [=swap period=] both expired before it finished loading. If the font is not used due to this, the user agent may choose to abort the font download, or download it with a very low priority. If the user agent believes it would be useful for the user, it may avoid even starting the font download, and proceed immediately to using a fallback font.
An ''optional'' font must never cause the layout of the page to "jump" as it loads in. A user agent may choose to slightly delay rendering an element using an optional font to give it time to load from a possibly-slow local cache, but once the text has been painted to the screen with a fallback font instead, it must not be rendered with the ''optional'' font for the rest of the page's lifetime.
The latest commit is not quite enough for font-display: optional
I think:
It says that the font should only be used if it "can be loaded 'immediately'". This needs to be clarified to allow for async loading from an on-disk cache, service worker, or other low-latency network source. I think it should be reworded to allow a "short" block period. The 100ms of bock periods elsewhere in the spec sounds fine to me.
It needs to say that the font blocks all rendering until it's known that the font won't come through in the block period, not just rendering of elements the font applies to.
The font needs to always be loaded, regardless of whether it applies to any elements on the page. This is necessary to avoid extra, inefficient forced style or layouts, and allow the fetch (either from cache or network) to start as soon as the font element is parsed. This also is needed to mitigate the "blocks all rendering" behavior of item 2 above.
Feedback from other vendors and community members is also needed for these proposed semantics.
The rationale for allowing low-latency network loads in addition to cache is:
A user agent may choose to slightly delay rendering an element
This seems to suggest that subsequent elements may still render.
We currently define font block period to mean 'layout the text using the fallback font but don't render glyphs'.
We need another thing that defines what browsers currently do when loading installed fonts. Is it a total render block? Eg will it block text selection throughout the page too? I'm assuming so. I'll refer to this as render block period.
Then, the 'block', 'swap' and 'fallback' strategies will have a small render block period, in addition to their font block period.
The 'optional' strategy will just have a render block period.
The tricky part is picking a number for the block time. UAs could even have a longer render block period before page load, where there's a lot of jank happening anyway.
UAs could skip the render block period if the browser is pretty sure the period limit would be reached (eg, font is not cached, and no service worker, and network looks slow).
It says that the font should only be used if it "can be loaded 'immediately'". This needs to be clarified to allow for async loading from an on-disk cache, service worker, or other low-latency network source. I think it should be reworded to allow a "short" block period. The 100ms of bock periods elsewhere in the spec sounds fine to me.
I'm not going to restore the exact language that I literally just removed due to complaints about it giving bad effects when read literally. ^_^
"Immediately" is scare-quoted and then immediately defined in terms of the necessary restriction it's imposing; as far as I can tell, that restriction (that the font be available in time for the first paint) allows all of the scenarios you mentioned? I very intentionally make no reference to the source of the font, precisely so that any of these can apply if the UA considers it reasonable.
It needs to say that the font blocks all rendering until it's known that the font won't come through in the block period, not just rendering of elements the font applies to.
That's a knock-on effect of the next paragraph, dictating that loading an optional font must not cause layout to jump. (This allows subsequent elements to be rendered if the UA can determine they can't depend on the layout of the optional-font element, like if the optional-font is used in a layout-contained element or something.)
The font needs to always be loaded, regardless of whether it applies to any elements on the page. This is necessary to avoid extra, inefficient forced style or layouts, and allow the fetch (either from cache or network) to start as soon as the font element is parsed. This also is needed to mitigate the "blocks all rendering" behavior of item 2 above.
I think this is an explicit anti-goal of this value; an "optional" font that is instead eagerly pre-loaded immediately on page load isn't very optional, I think. ^_^ Note the other functionality attached to the value, allowing the UA to abort the font load entirely if they know they're on a low-bandwidth connection, for example. An "optional" font is explicitly intended to be unreliable; the feature is aimed at being maximally friendly to users at the expense of authors' design preferences.
An author can already preload fonts explicitly using the existing meta-preload functionality; combining that with optional
to give you a better chance of hitting the "immediately available" window is a perfectly reasonable idea.
This seems to suggest that subsequent elements may still render.
See response to Chris's second point, above.
We need another thing that defines what browsers currently do when loading installed fonts. Is it a total render block? Eg will it block text selection throughout the page too? I'm assuming so.
It is not, at least in Chrome. Loading an installed font from the disk cache can, today, result in a flash-of-unfonted-text for a frame or two if the call is slow. I think loading the generic fonts is blocking for rendering, tho. @xiaochengh has the details; I'm probably badly recounting what I remember them telling me.
The font needs to always be loaded, regardless of whether it applies to any elements on the page. This is necessary to avoid extra, inefficient forced style or layouts, and allow the fetch (either from cache or network) to start as soon as the font element is parsed. This also is needed to mitigate the "blocks all rendering" behavior of item 2 above.
I think this is an explicit anti-goal of this value; an "optional" font that is instead eagerly pre-loaded immediately on page load isn't very optional, I think. ^_^ Note the other functionality attached to the value, allowing the UA to abort the font load entirely if they know they're on a low-bandwidth connection, for example. An "optional" font is explicitly intended to be unreliable; the feature is aimed at being maximally friendly to users at the expense of authors' design preferences.
How should a UA decide whether/when to load a font-display: optional
font?
According to the font loading guidelines, UA should "download" a font only when "used within a document". However, we can't know whether a font is used without actually rendering the document. And if we render the document, we are basically violating the "no relayout" requirement of optional
. Even if we don't deliver the frame (so that layout doesn't jump), we are still introducing extra latency that we want to avoid.
So If authors want to ensure no extra rendering while maximizing the chance that the font is used, do they have to force loading it with Font Loading API or <link rel="preload">
, in combination with optional
?
(Or do words "download" and "load" have different meanings here?)
I feel like there are two different use cases where we want to avoid relayout and extra rendering latency:
The latest spec revision seems to solve the latter. Should we introduce something like font-display: important
for the former?
However, we can't know whether a font is used without actually rendering the document.
I don't understand what you mean here; this seems obviously false. You know when a font is used after styling a document; the render is much later in the pipeline.
So If authors want to ensure no extra rendering while maximizing the chance that the font is used, do they have to force loading it with Font Loading API or
<link rel="preload">
, in combination with optional?
Yes. That'll give the page vital milliseconds to pull down the font (or load it from disk cache) before the page is otherwise renderable, maximizing the chance it gets selected.
The latest spec revision seems to solve the latter. Should we introduce something like font-display: important for the former?
The optional
value was always the latter; the name, the description, and the mechanics were all firmly aimed at "I'd like to use this font but it's totally okay to render the text in whatever's available quickly".
font-display: important
is just font-display: block
, no? Or if it's not, that's because it's actually even more restrictive than block
, blocking rendering of the entire page until the font is delivered (rather than just the text)?
Given that we already widely agree that block
is a bad, user-hostile behavior, I don't see how an even more hostile behavior would be something we want to add.
I don't understand what you mean here; this seems obviously false. You know when a font is used after styling a document; the render is much later in the pipeline.
That's false. You need to do font fallback in order to know whether a font is used. So the trigger of the font load happens much later than styling.
You could eagerly download all the web fonts that appear in any family list for all possible unicode ranges, but that's obviously not great.
However, we can't know whether a font is used without actually rendering the document.
I don't understand what you mean here; this seems obviously false. You know when a font is used after styling a document; the render is much later in the pipeline.
Sorry if I'm not using the accurate terms.
In that way, we still need a forced style recalc to decide whether to load the font. When to do that remains quite tricky. Besides, that's not a cheap task, and the computed style will be invalidated after the font loads (which is at least what's happening in Blink), which is something we'd like to avoid.
So If authors want to ensure no extra rendering while maximizing the chance that the font is used, do they have to force loading it with Font Loading API or
<link rel="preload">
, in combination with optional?Yes. That'll give the page vital milliseconds to pull down the font (or load it from disk cache) before the page is otherwise renderable, maximizing the chance it gets selected.
The latest spec revision seems to solve the latter. Should we introduce something like font-display: important for the former?
The
optional
value was always the latter; the name, the description, and the mechanics were all firmly aimed at "I'd like to use this font but it's totally okay to render the text in whatever's available quickly".
font-display: important
is justfont-display: block
, no? Or if it's not, that's because it's actually even more restrictive thanblock
, blocking rendering of the entire page until the font is delivered (rather than just the text)?Given that we already widely agree that
block
is a bad, user-hostile behavior, I don't see how an even more hostile behavior would be something we want to add.
I don't think it's another block
. I'm think of the following:
@font-face
rule is added
Authors should use it only when they want to maximize the chance to use locally cached fonts while reducing latency and layout shifting.
Maybe I should have called it font-display: cache-friendly
earlier to better express it 😅
I don't think it's another block. I'm think of the following:
Is this actually different from:
font-display:optional
optional
.?
Also, what do you plan to do for ex units? do you plan to block styling on the font metrics being available?
That's false. You need to do font fallback in order to know whether a font is used. So the trigger of the font load happens much later than styling.
You could eagerly download all the web fonts that appear in any family list for all possible unicode ranges, but that's obviously not great.
Sure, but no fallback is needed for the first font in the list, which is what I'm most concerned about here: declarations that are just font-family: webfont, sans-serif;
.
(If you have a whole stack of fonts it shouldn't be a surprise that later ones, being clearly less important, might get skipped by "optional".)
As you mentioned earlier, optional
is for low priority fonts. It looks pretty hacky to use preload + optional
to handle high priority fonts.
As you mentioned earlier, optional is for low priority fonts. It looks pretty hacky to use preload + optional to handle high priority fonts.
What you described isn't high priority, tho! You described a situation where a font that takes longer than 100ms never gets used. That's clearly still an "optional" font; it's nice-to-have but not vital.
optional
is intended to prioritize the user experience (no layout jumping) over the authoring design intent (pretty fonts). If the author does nothing else, it can also prioritize the user's bandwidth, but that's listed as a secondary, very much optional additional effect. Preloading is a common and reasonable technique where we let the author control the user experience a little (kicking off downloads eagerly) in return for a hopefully better UX later (things being visible immediately when used).
Combining the two seems totally reasonable to me.
Also, what do you plan to do for ex units? do you plan to block styling on the font metrics being available?
How do you handle ex units today, when the font is already downloaded and in cache?
optional
is intended to prioritize the user experience (no layout jumping) over the authoring design intent (pretty fonts).
I'm not convinced this design does a good job of that. There are other aspects of user experience that are also relevant here: for example, I think users would be bothered by having bold and/or italic text in a fallback font when the primary text was in the downloaded font. (There might also be problems with accented characters for, e.g., Latin-script locales that use characters outside of Latin1, depending on how fonts are typically chunked for download.)
I'd love to be able to give web devs the advice "apply font-display:optional and this won't be a problem", but today we can't do that.
I think this might be plausible "won't be a problem" advice for pages that use only a single font face (no weight variation, no italic/oblique variation) and only use ASCII characters (or perhaps a slightly broader Latin set depending on how the fonts they use are provided). But I think anything more advanced, which is a lot of the Web, is going to involve tradeoffs here.
(And remember that a bunch of the existing design of downloadable fonts is about trying not to download a ton more fonts than needed, given the reality of multiple weights, multiple styles, and large character ranges.)
Thanks for your thoughts @dbaron - the multiple font single typeface case is an interesting one.
The primary offender I've seen here has been for headings in a webfont, which are generally in a single font, without any variations. That said, most of my digging here has focused on pages with latin scripts.
I agree that that we'll need to enable the developer to make tradeoffs here. I think the proposed semantics here around optional
is one configuration we should support. Does that seem reasonable? Is there a use case for the current definition of optional
that we'd lose by making this change?
@dbaron
here are other aspects of user experience that are also relevant here: for example, I think users would be bothered by having bold and/or italic text in a fallback font when the primary text was in the downloaded font.
That's an important use-case, but I think it's tangential, and a much bigger problem that should probably be spun off into its own issue. Seems like you want to synchronise the loading of all faces of a particular font family. Feels like you'd need to apply this behaviour to a particular element, and you'd need to have the whole element to know what needed to synchronise, so it'd affecting streaming rendering.
@tdresser
Is there a use case for the current definition of optional that we'd lose by making this change?
I still think we need to define how browsers render when they load fonts from disk. I've never seen this create layout stability issues, but maybe there are extreme cases where it does.
Either way, this is the behaviour we want for optional
, but with an aggressive timeout where it switches to a fallback.
Yeah, the "load/use all the faces of this font, or none of them" is a reasonable thing to want, but it's definitely an orthogonal issue; you'd want that control for non-optional
fonts as well. So unless there's an important connection to the current topic that I'm not seeing, I'd like to push that to another issue.
I still think we need to define how browsers render when they load fonts from disk. I've never seen this create layout stability issues, but maybe there are extreme cases where it does.
Per @xiaochengh, loading from memory cache is always fine, but loading from disk cache sometimes (often?) misses the first frame's layout/painting, so the second frame has a layout jump. It might be that a single frame of jump is perfectly fine tho; I want to hear from people currently experiencing that (we've got some internal customers for this behavior, apparently, so I'll be talking to them), and if it's okay, make sure the spec text is flexible enough to allow for it. (Currently it's not; if you miss the first paint you'll never use the font. It just lets you delay the first paint a little if you want to give it a chance to finish loading.)
Okay, just wrapped up a meeting with @chrishtr and @xiaochengh to discuss the minutiae of our implementation. I'll summarize here, with our preferred conclusion, and other browsers can chime in if there's a disconnect with their own impls.
When you're processing styling and realize a font is needed, we kick off a load. If the font is in memory cache (generally per-tab), this load is synchronous, and takes <1ms. Otherwise (if it's in disk cache, or not cached at all) the load is asynchronous, and we proceed without the font for this frame. (Next frame it might get picked up, assuming it loaded fast enough; disk cache should generally return before a second frame is started.)
Memory cache is pretty ephemeral; usually it'll stick around as long as a tab is alive, but it can evict at any time (so subsequent same-origin navigations might have it available, or might not). If you close and re-open the site, it'll usually, but not always, be evicted.
This means that, if we don't delay the render process, much of the time an optional
font will be skipped, especially on the first navigation to a page.
This matches up with what I intend the value for; the way I use it on my blog, for example (as a pure aesthetic upgrade to heading and code font), it's perfectly okay for it to often get skipped and render with the system fonts.
But some use-cases, reasonably, would like a slightly greater assurance that the font will be used. It seems to us that a reasonable signal is whether the font is preloaded - that's a pretty strong signal that the font is expected to be used and important enough to start loading early.
So our plan is:
make sure that font preloads cause the font to be put in the tab's memory cache
track which fonts are being preloaded this way. During the "load font" check, if the font's not currently in memory cache, check if it's on the list of preloaded fonts. If so, do a blocking load for a small amount of time (20-30ms, maybe?) to give it time to finish loading into memory. Keep a bit around to track whether this happened, so it can't reoccur serially due to multiple fonts needing loading; it only delays once, and if additional preloaded fonts aren't ready yet, too bad.
otherwise act like optional
normally does
This means that if you preload your optional
font, there's a very high chance that it'll be used as long as it's been cached appropriately (or it's served off of a fast SW, or even network-loaded from an extremely fast network); the only time it'll likely be skipped is on first visit when it has to be network-loaded.
In terms of spec changes, I think I can get away with just adding a SHOULD about preloaded fonts, probably phrased more vaguely as "fonts for which the page has signaled ahead of time are likely be used", keying the "delay the first paint of the page" off of that.
How does this sound to people? Do other browsers have loading architectures that differ significantly from what I described, and would require different handling?
I think this captures almost all of it (doesn't seem to capture cached fonts that happened to not have a preload meta tag?).
How about this:
Let's call a font cached-locally
if no network request (Service Worker not allowed either) need be done to get the resource.
Let's call a font short-render-blocking
if it is referenced by a preload tag in the HTML document, or is cached-locally
.
If, while parsing an HTML document, we encounter a reference to a short-render-blocking
font, then the User Agent should delay subsequent rendering for a short period (< 100ms) in order to give that font time to load. If a font is cached-locally
, the User Agent should move the font into memory as soon as it is discovered in the stylesheet.
That's it. If a developer wants their font to have the highest likelihood of displaying on cold and warm loads, and never have double renders due to that font, they should put something like this in their HTML:
HTML:
<-- This causes the font network fetch to start as soon as possible, supports caching
in the local HTTP cache or service worker, and causes the font to be
short-render-blocking -->
<link rel="preload" href="my-font.woff" as="font">
...
<link rel="stylesheet" href="my-stylesheet.css">
...
Content that depends on my-stylesheet.css
my-stylesheet.css:
@font-face {
font-family: ExampleFont;
src: url(my-font.woff) format('woff'),
# This causes the font to never cause double rendering
font-display: optional;
}
Perhaps this should also apply to all web fonts. font-display: optional
is perhaps special only in that we should never render twice due to it, regardless of whether it was available in memory for the first render.
Some more recap of our meeting...
There can be some issues if we try to load all cached-locally
fonts on parsing, especially when using a web font library. We can end up loading too many unused fonts into memory. We are still good if we only delay rendering for the fonts being preloaded.
Also @drott for insights.
Good point. Perhaps we should say may instead of should for "move the font into memory as soon as it is discovered in the stylesheet", and only leave should for ones that have a preload tag.
This would allow UAs to only do so if there is memory budget, or there is good reason to think the font is definitely needed, or some other clever heuristic. Or allow them to just not do that and still be compliant.
@tabatkins wrote:
Yeah, the "load/use all the faces of this font, or none of them" is a reasonable thing to want, but it's definitely an orthogonal issue; you'd want that control for non-optional fonts as well.
Except I'm not sure the "load" part of it really is reasonable, given large numbers of faces (e.g., for weights or character ranges).
It might be that some faces for a font are already in use (and were, say, already cached), and a change occurs to require an additional face (say, new text in a new character range, or something in a different weight or style). It seems like in that case you (a) probably don't want to flash the already-loaded fonts back to their fallback either temporarily or permanently, but different developers may have substantially different feelings between (b) showing fallback temporarily for the new characters and then switching versus (c) showing fallback permanently for the new characters but continuing to use the downloaded font for the faces that were already displayed.
The CSS Working Group just discussed font-display
.
I wasn't there in person, and AIUI this issue will be discussed again tomorrow, which is great. In the meantime, I'd like to repeat the three driving use-cases from my perspective:
() Note: this does not* say anything about cases where the font is not available locally on the device.
+1. Also, it'd be nice if the solution also prevented layout instability when loading an inlined base64-encoded font.
The CSS Working Group just discussed font-display: optional
, and agreed to the following:
RESOLVED: change normative text for font-display optional to say that the font should never change rendering of the page if it would you'd still just treat the load as failed and don't use it again
RESOLVED: Add notes for implementors and authors to the spec, specific contents TBD
@tabatkins can you propose text for this resolution (as you were more involved in the issue discussion) or do you want me to take a crack at it? Or @jakearchibald want to propose text? Can be here in the issue for discussion, does not need to be a PR.
Looking over the spec again, the previous normative text (from Jan 6 2020) already addresses the first resolution.
I've just added the requested note suggesting some heuristics, while making it clear that the heuristics cannot be relied upon.
https://drafts.csswg.org/css-fonts-4/#font-display-desc
I originally thought
font-display: optional
was designed to provide web fonts in a way that avoided swapping or flash-of-invisible-text, at the expense of getting a fallback font for the first visit. This is kinda backed up by a note in the spec:However, the prose doesn't really define the behaviour.
https://static-misc.glitch.me/optional-font-load/
In Chrome & Firefox at least, the page may briefly render with invisible text (using layout from a fallback font), causing the green bar to move when the page is laid out again using the web font. This is the kind of layout instability I thought
font-display: optional
was designed to avoid.Proposal:
When the browser discovers a part of the page that needs a
font-display: optional
web font, it must:"only-if-cached"
fetch for the resource.font-family
stack.We already block rendering when getting regular font resources from disk, so blocking rendering shouldn't really be a problem here, unless we expect the above to be significantly slower than loading installed fonts.
Do you think we can safely change
font-display: optional
to behave similar to above? If not, do we need a new value that behaves like this?