Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webfonts: Preload fonts in the head to stop a flash of unstyled content #39391

Closed
wants to merge 6 commits into from

Conversation

scruffian
Copy link
Contributor

@scruffian scruffian commented Mar 11, 2022

What?

This adds a preload for each Webfont that is loaded in theme.json so that the fonts load before the text is displayed

Why?

Without this PR the text on the site that uses these fonts is displayed in the default font until the font files load. If the font files load slowly they aren't used until the subsequent page views.

How?

This adds a link tag to the header for each font file of the form:
'<link rel="preload" href="' . $src . '" as="font" type="font/' . end( $srcAsArray ) . '" crossorigin />'

Testing Instructions

This is quite tricky to test.

  1. Use a theme that loads webfonts in theme.json (you can use one from this PR)
  2. If necessary remove any code that preloads webfonts (there is some in the themes in the PR above)
  3. Also add preload: true to one of the font files
  4. Open you developer console and go to the network tab
  5. Disable cache
  6. Set your connection to be slow
  7. Load the frontend of your site. With this PR you'll notice that the text doesn't load until the font files have loaded. On trunk you'll notice that you see the text in the default font.
  8. Check the markup and you'll see that the font file you specified has been loaded in a link element with a rel attribute of preload.

Screenshots or screencast

Before:
before

After:
after

@scruffian scruffian added the [Feature] Themes Questions or issues with incorporating or styling blocks in a theme. label Mar 11, 2022
@scruffian scruffian self-assigned this Mar 11, 2022
@scruffian scruffian requested a review from spacedmonkey as a code owner March 11, 2022 16:04
Copy link
Member

@mikachan mikachan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as described for me, however, I'm not sure this will work well for themes that include many webfonts.

As this automatically preloads all fonts, I believe this could actually have a negative effect on performance. Ideally, we only want to preload the main font/s or the most used font/s. The other fonts may not even need to be downloaded, depending on if there is any content using them.

When I tried this on Archeo, it preloads all 6 font files, but there are only 2 being used in the theme by default.

I'm not sure what the fix would be for this - maybe we could add a 'main font' / 'preload' setting to theme.json where the fonts are described (something like fontFace.preload = true)?

@scruffian
Copy link
Contributor Author

I'm not sure what the fix would be for this - maybe we could add a 'main font' / 'preload' setting to theme.json where the fonts are described (something like fontFace.preload = true)?

What if we just preload the first one?

@mikachan
Copy link
Member

What if we just preload the first one?

Yeah that'd work! Although there could be times when it'd be beneficial to load more than one (in Archeo, there are two fonts used quite high up the page by default). But I guess this could vary so much, and I like how simple just preloading the first one is.

@scruffian
Copy link
Contributor Author

I'm not sure what the fix would be for this - maybe we could add a 'main font' / 'preload' setting to theme.json where the fonts are described (something like fontFace.preload = true)?

Done!

@scruffian
Copy link
Contributor Author

If we go this route we should add a test for it...

draganescu
draganescu previously approved these changes Mar 14, 2022
Copy link
Contributor

@draganescu draganescu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good step. I faced "the flash" problem. To preload perfectly we'd need more granular controls, (maybe at a template level?) I would start with preloading everything.

Copy link
Member

@aristath aristath left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick review, and added a couple of notes.
There is something else I wanted to mention though...

It seems to me that the preload_webfonts method should be a part of the WP_Webfonts_Provider_Local class and not the main WP_Webfonts class.
The changes here only apply to fonts hosted locally, and though that may be the only case we have now, there's no guarantee it will remain like that in the future.

@@ -165,6 +173,7 @@ public function validate_font( $font ) {
'font-feature-settings',
'font-variation-settings',
'line-gap-override',
'preload',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since preload is not a valid CSS prop, I think it would be best if we added it to the // exceptions section a few lines below, along with provider.
We'd also need to add a check in the local-provider class to make sure that no CSS is generated for this:

// Skip "provider", since it's for internal API use,
// and not a valid CSS property.
if ( 'provider' === $key ) {
continue;
}

lib/compat/wordpress-6.0/class-wp-webfonts.php Outdated Show resolved Hide resolved
lib/compat/wordpress-6.0/class-wp-webfonts.php Outdated Show resolved Hide resolved
if ( ! in_array( $file_type, self::$allowed_types ) ) {
$file_type = '';
}
echo '<link rel="preload" href="' . $src . '" as="font" type="font/' . $file_type . '" crossorigin />';
Copy link
Member

@aristath aristath Mar 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WP_Webfonts_Provider_Local::order_src() method returns an array of URLs along with their file/mime types. Perhaps we should use that? It would make sense, especially if we move the implementation to the WP_Webfonts_Provider_Local object (where this should be IMO)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WP_Webfonts_Provider_Local::order_src() method returns an array of URLs along with their file/mime types. Perhaps we should use that?

Thinking about it, yes, this is a better approach to avoid redundancy.

I agree that this method is a better fit for the local provider (WP_Webfonts_Provider_Local) than here. In the future when Font Factory providers are bundled, this code will need changes as src is specific to locally hosted fonts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is rendering the HTML, the variables require escaping for security:

Suggested change
echo '<link rel="preload" href="' . $src . '" as="font" type="font/' . $file_type . '" crossorigin />';
printf(
'<link rel="preload" href="%s" as="font" type="font/%s" crossorigin />',
esc_url( $src ),
esc_attr( $file_type )
);

@aristath
Copy link
Member

On a personal note: The use of preload was part of some initial implementations, but the more we kept working on it, decided against using it.

We already have font-display, which can have values like auto, block, swap, fallback, optional.
If there is FOUT, that's probably because the default value of font-display is fallback, and we have not defined a different value for that prop when declaring the webfont.

It's also important to test what the impact of preload would be on slow connections. Not everyone is blessed with fast internet, and there's a chance that adding preload will make things worse for a slow/metered 3G connection, delaying the display of the content itself for the sake of styling.

Comment on lines +313 to +314
$src_as_array = explode( '.', $src );
$file_type = end( $src_as_array );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-optimization suggestion:

Suggested change
$src_as_array = explode( '.', $src );
$file_type = end( $src_as_array );
$file_type_pos = strrpos( $src, '.' ) + 1;
$file_type = substr( $src, $file_type_pos );

Why? explode creates an array which consumes a bit more memory and time. To see it in action, here's a benchmark running on PHP 7.4 over 1 million cycles https://3v4l.org/EnOB1#v7.4.28.

Results:

Implementation Average μs
explode and end 0.2440 μs
strrpos and substr 0.0959 μs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW This code is not needed if the preload functionality instead grabs the font srcs from the provider's WP_Webfonts_Provider_Local::order_src() as suggested here.

*/
public function preload_webfonts() {
foreach ( $this->get_fonts() as $font ) {
if ( ! empty( $font['preload'] ) && $font['preload'] ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is deeply nested causing the code to shift to the right. This can decrease readability. I'd suggest either breaking it up into private methods -or- converting this guard into a skip clause like this:

Suggested change
if ( ! empty( $font['preload'] ) && $font['preload'] ) {
// Skip this font if it's not configured for preloading.
if ( empty( $font['preload'] ) ) {
continue;
}

@hellofromtonya
Copy link
Contributor

hellofromtonya commented Mar 15, 2022

I left comments on the implementation itself. There are improvement opportunities for performance and readability.

It's also important to test what the impact of preload would be on slow connections. Not everyone is blessed with fast internet, and there's a chance that adding preload will make things worse for a slow/metered 3G connection, delaying the display of the content itself for the sake of styling.

I would also advise benchmarking preload as @aristath suggests #39391 (comment). There are performance impacts on slower connections and potentially impacts on all connections when preloading too many fonts.

@aristath and I previously discussed a future enhancement to discover the fonts on the page and then only process those fonts. If preloading were added, then such an approach could focus on only preloading the actual needed fonts rather than all of the registered fonts. This discovery type of functionality adds complexity, but should be doable. It is beyond the scope of this PR as it'll need extensive discussion, experimentation, and testing.

If there is FOUT, that's probably because the default value of font-display is fallback, and we have not defined a different value for that prop when declaring the webfont.

Before adding preloading, would be good to investigate and focus here if this is the root cause of the FOUT.

@mikachan
Copy link
Member

I understand the need to investigate the cause of the FOUT, however, I believe the solution will vary for different themes and users.

The decision to preload specific fonts depends on how important a user/themer determines the font to be, maybe for usability, the overall performance, or the consistency of the design. For example, if there are two fonts that are deemed important for usability on initial load, a user may choose to preload these with a combination of a font-display property. Or, a user who doesn't see their webfonts as necessary styling could choose not to preload at all (and still use font-display for further control).

By adding a preload option, we're allowing users to make their own decisions about their site performance, i.e. speed vs design fidelity. I think a good way to handle this is to not preload anything by default, and just offer the option to preload fonts in theme.json.

@draganescu draganescu dismissed their stale review March 15, 2022 23:13

Better reviews followed.

@scruffian
Copy link
Contributor Author

Let's close this for now. We can come back to it again if there is a need in the future.

@scruffian scruffian closed this Mar 17, 2022
@scruffian scruffian deleted the add/preload-webfonts branch March 17, 2022 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Themes Questions or issues with incorporating or styling blocks in a theme.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants