-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Conversation
There was a problem hiding this 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
)?
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. |
Done! |
If we go this route we should add a test for it... |
There was a problem hiding this 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.
There was a problem hiding this 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', |
There was a problem hiding this comment.
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:
gutenberg/lib/compat/wordpress-6.0/class-wp-webfonts-provider-local.php
Lines 193 to 197 in 9f5fb53
// Skip "provider", since it's for internal API use, | |
// and not a valid CSS property. | |
if ( 'provider' === $key ) { | |
continue; | |
} |
if ( ! in_array( $file_type, self::$allowed_types ) ) { | ||
$file_type = ''; | ||
} | ||
echo '<link rel="preload" href="' . $src . '" as="font" type="font/' . $file_type . '" crossorigin />'; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
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 ) | |
); |
On a personal note: The use of We already have It's also important to test what the impact of |
Co-authored-by: Ari Stathopoulos <[email protected]>
Co-authored-by: Ari Stathopoulos <[email protected]>
$src_as_array = explode( '.', $src ); | ||
$file_type = end( $src_as_array ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Micro-optimization suggestion:
$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 |
There was a problem hiding this comment.
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'] ) { |
There was a problem hiding this comment.
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:
if ( ! empty( $font['preload'] ) && $font['preload'] ) { | |
// Skip this font if it's not configured for preloading. | |
if ( empty( $font['preload'] ) ) { | |
continue; | |
} |
I left comments on the implementation itself. There are improvement opportunities for performance and readability.
I would also advise benchmarking @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.
Before adding preloading, would be good to investigate and focus here if this is the root cause of the FOUT. |
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 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 |
Let's close this for now. We can come back to it again if there is a need in the future. |
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.
preload: true
to one of the font filesrel
attribute ofpreload
.Screenshots or screencast
Before:
After: