KenjiBaheux / css-font-rendering

A proposal for CSS to let web developers control the behavior of font rendering
Apache License 2.0
96 stars 10 forks source link

Bedrock for CSS font-rendering? #20

Open KenjiBaheux opened 9 years ago

KenjiBaheux commented 9 years ago

The CSS font-rendering proposal is an attempt to explain the bit of magic that goes on around web font rendering in browsers:

Browser timeout fallback swap
Chrome 35+ 3 seconds yes yes
Opera 3 seconds yes yes
Firefox 3 seconds yes yes
Internet Explorer 0 seconds yes yes
Safari N/A N/A N/A
  • Chrome and Firefox have a 3 second timeout after which the text is shown with the fallback font. Eventually, a swap occurs: the text is re-rendered with the intended font once it becomes available.
  • Internet Explorer has a 0 second timeout which results in immediate text rendering: if the requested font is not yet available, fallback is used, and text is rerendered later once the requested font becomes available.
  • Safari has no timeout behavior (or at least nothing beyond a baseline network timeout of 60(?) seconds)

A discussion around the adequacy of the name "font-rendering" lead to the realization that we have a hard time identifying/naming the underlying system involved here. This suggests that CSS font-rendering might not be ambitious enough or might not explain the actual magic but a specific manifestation of it. I think this is worth investigating in parallel with making progress on the proposal and adjust as we learn more.

Relation to Houdini to/ @shans @bfgeek

To start, we should find out if Houdini (bedrock for CSS) would impact this proposal, otherwise we run the risk of turning CSS font-rendering into The Magic Trick That Fooled Houdini ಠ_ಠ

Is the CSS font-rendering use case (a specific manifestation of the magic that goes into holding paint) in scope for Houdini? Anything started in this area?

CSS font-rendering example

@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 400;
  src: url(//example.com/opensans/normal400.woff2) format('woff2');

  /*
    set default font-rendering strategy
    - don't block text rendering on this font
    - abandon progressive rendering after 2s
  */
  font-rendering: swap 2s;
}

body { font-family: Open Sans; }

#header {
  /* block rendering until the desired font is available (due to branding requirements).
     if branding font is not ready within 500ms, use fallback */
  font-rendering: block 500ms;
}

#headline {
  /* immediately render the headlines, if the desired font is available, great...
     and if not, use fallback and don't rerender to minimize reflow */
  font-rendering: optional;
}

#main-content {
  /* don't hold rendering, but rerender with the desired font once available */
  /* give up on progressive strategy after 150ms due to UX+perf requirements */
  font-rendering: swap 150ms;
}

#footer {
  /* inherits font-rendering: swap */
}

cc/ @natduca @igrigorik @tabatkins

bfgeek commented 9 years ago

@KenjiBaheux yeah that's pretty magical.

Does this block just paint or layout as well? For example can I query offsetWidth of an element which doesn't have a loaded font yet?

Re: Houdini, nothing in the spec w.r.t. holding paint at the moment, however we've had discussions regarding when to paint (I.e. if blocking is a reasonable option) in addition to blocking layout. Element optionality and asynchronous in-flow elements (if we end up doing them) will need similar ideas.

If we had some kind of paint blocking primitive then font-rendering could be implemented entirely as a user-land property on top of Houdini.

Do we have anything else which can block rendering on the page? For example:

Are there things that we should allow blocking rendering? For example:

Ian

tabatkins commented 9 years ago

@bfgeek Before a font loads, we're either rendering with a fallback, or with some sort of "default" metrics (depending on whether we're in the block or swap period). So nothing is blocked; we can paint immediately, query things immediately, etc. (In other words, during the "block" period, you fallback to an invisible default font which has no glyphs.)

KenjiBaheux commented 9 years ago

@bfgeek @tabatkins 's description is more accurate.

Perhaps, "custom paint" and "custom layout" are more relevant then?

If we can explain all font-rendering behaviors via custom paint and custom layout callbacks, can we consider this issue resolved?

Naively, I imagine that it can be achieved through a combination of:

Let me try a few

For an element whose style is set with font-rendering: optional: Have a custom layout callback where:

For an element whose style is set with font-rendering: block infinite: Have a custom paint callback where:

For an element whose style is set with font-rendering: block 0s swap 500ms: Have a custom layout callback where:

Have a custom paint callback

How does that sound?

cc/ @igrigorik

igrigorik commented 9 years ago

@KenjiBaheux I think that's spot on. My only nitpick would be with the last 500ms timer logic:

set a race between a (500ms - performance.now()) timeout and a Font Loading check() to find out if the desired font is ready before timer elapsed...

That is, I think we should think of the specified "500ms" timer as counting from navigationStart, instead of requestStart, because requestStart is a moving target and hard to reason about.

tabatkins commented 9 years ago

That is, I think we should think of the specified "500ms" timer as counting from navigationStart, instead of requestStart, because requestStart is a moving target and hard to reason about.

Is that consistent with what browsers actually do? My spec currently dictates that the timer starts at requestStart, because it doesn't seem to make much sense for a font that isn't used on load to have its timer already-expired when it is finally invoked.

igrigorik commented 9 years ago

@tabatkins consistent with respect to what? I think both start times are valid (navStart, requestStart) and have their respective use cases. That said, I think navigationStart is what developers would expect/want in the context of optimizing rendering behavior of the page...

The problem with fetchStart is that you actually have no idea when the request will start, so "3s" from fetchStart is actually 3s + unknown-fetch-start-delay, where the unknown delay can be easily measured in seconds -- e.g. style calc is blocked on slow response of a script, other css file, etc. As such, this is not terribly useful: an optional font may still be blocked for some time; all timers have an unknown value added to them. If my goal is to deliver a reliable 1000ms render (with desired font, or fallback), I can't do that with font-rendering that uses fetchStart, since I don't know when fetchStart will fire.

tabatkins commented 9 years ago

Making font load behavior depend on document load time, so that a timer can expire before you ever even attempt to load the font file doesn't sound useful. If you think there is a use-case, coudl you elaborate?

You seem to be thinking about the difference in time between "I've activated the style that applies the font" and "the font load is started", but it's actually the difference in time between "the page is loaded" and "the font load is started". If I only use a particular font on error messages, and the user doesn't cause an error message until they've spent a minute or two on the site, that's 100s or more of difference between the two times.

igrigorik commented 9 years ago

You're right, the timer business is complicated.

My primary goal is to ensure that we enable developers to deliver the fastest possible experience in their application. In the case of text-rendering, this means giving them control over which text rendering strategy is chosen when the text is first ready to paint - i.e. should the UA paint it now and swap in the font, should the UA block and paint later, or should it paint with whatever is available and leave it. This decision / property is independent of any timers.

Some concrete applications of this are:

Stepping back, perhaps the timer semantics can be separated from the control of the chosen font-rendering strategy? As in, block / swap / optional are properties on the text, whereas timer is a property of the font? Hand~wavy example...

@font-face { 
  font-family: 'Open Sans';
  ...
  font-timeout: 3s;
} 

.content { font-rendering: swap; font-family: Open Sans; } 
.other { font-rendering: block; font-family Open Sans; }

The font-rendering controls which strategy is chosen when text is first ready to paint, and timeout is controlled by the @font-face property? This model has its limitations as well, but it feels "more right"?

On Wed, Apr 8, 2015 at 1:18 PM, Tab Atkins Jr. jackalmage@gmail.com wrote: (www-style) In the call today, we agreed to initially give the spec only the keyword values, with an issue discussing whether or not to allow explicit timeouts.

Hmm, I think this is effectively what I'm proposing above, except that font-rendering applies to @font-face as well.. perhaps that's unnecessary?

natduca commented 9 years ago

Why can't the timeout be done in js? We set a timer to change the style...

natduca commented 9 years ago

What do y'all think about a shared google doc for "can font rendering be done with houdini?" Its easier to collaborate on a proposal in that format, ask questions, share around to other people, than with a centithread.

esprehn commented 9 years ago

I think the FontLoader API + setTimeout is enough for JS to deal with this. +1 to discussing it in a doc.

KenjiBaheux commented 9 years ago

FontLoader API and setTimeout being enough is missing the point.

https://lists.w3.org/Archives/Public/www-style/2014Oct/0505.html

tabatkins commented 9 years ago

Why can't the timeout be done in js?

Because you don't know when the timer started? It starts when we first attempt to use the font. To get the same behavior, you have to not use the font at all withing your CSS at first, and instead invert your design so that the element pings some observer about what font it needs when it first knows it's going to render (somehow, perhaps a more explicit "display me" API, which means you're avoiding use of the 'display' property too...).

In general, all of this stuff can be done with the Font Loading API, to some degree of fidelity. It's not trivial, but it's doable. However, attempting to argue that this means we don't need anything in CSS is completely wrong-headed, for several reasons:

  1. The default behavior today, blocking until loaded or timeout, is bad for users. Saying that the only way to fix it is to do a moderately difficult bit of work in JS just means that it'll continue being bad for most users.
  2. This problem is particularly bad for fonts used immediately, on page load. Requiring authors to handle font-loading behavior in JS means that you have to wait for the JS to load, meaning you're blocking font loading behind at least another RTT of network delay, which is the exact opposite of what we want. (Or you're requiring that authors embed font-loading JS directly in their HTML, early in the response.)
  3. Doing this with any degree of fidelity requires much larger changes to the page architecture than you're probably imagining, with elements taking over much more responsibility for displaying themselves than CSS offers. This is basically reinventing a bunch of CSS, violating the "don't make people rebuild from the bottom" aesthetic that all the "explain the platform" stuff is trying to maintain. It's possible to add more API that makes this less bad - for example, some way to observe when an element is about to paint for the first time - but I don't think we should forced to invoke Custom Paint on every element on the page just to make fonts not block rendering.