-
Notifications
You must be signed in to change notification settings - Fork 101
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
Prioritize loading fonts for textual LCP elements #1313
Comments
I'm thinking about how this would be implemented in practice. It seems it would rely on calling ( textElement ) => {
const cssFontFaceRules = [];
const stripQuotes = ( str ) => str.replace( /^"/, '' ).replace( /"$/, '' );
const computedStyle = getComputedStyle( textElement );
const fontFamilies = computedStyle.fontFamily.split( /\s*,\s*/ );
const fontFamily = stripQuotes( fontFamilies[0] );
for (const sheet of document.styleSheets) {
for (const rule of sheet.cssRules) {
if (rule.constructor.name === 'CSSFontFaceRule' && fontFamily === stripQuotes( rule.style.fontFamily )) {
cssFontFaceRules.push( rule );
}
}
}
return cssFontFaceRules;
} But note there can be multiple fonts that have the same @font-face {
font-family: "Inter var";
font-weight: 100 900;
font-style: normal;
font-display: swap;
src: url(./assets/fonts/inter/Inter-upright-var.woff2) format("woff2");
}
@font-face {
font-family: "Inter var";
font-weight: 100 900;
font-style: italic;
font-display: swap;
src: url(./assets/fonts/inter/Inter-italic-var.woff2) format("woff2");
} So when determining the font file to prioritize loading, it would also need to look at the computed style to find the weight and style to determine which variant of the font should actually be preloaded. Nevertheless, there could also be duplicate @font-face {
font-family: "Inter var";
font-weight: 100 900; /* stylelint-disable-line font-weight-notation */
font-style: normal;
font-display: swap;
src: url(../fonts/inter/Inter-upright-var.woff2) format("woff2");
}
@font-face {
font-family: "Inter var";
font-weight: 100 900; /* stylelint-disable-line font-weight-notation */
font-style: italic;
font-display: swap;
src: url(../fonts/inter/Inter-italic-var.woff2) format("woff2");
} So these two stylesheets are duplicating the The last one encountered should be used since it wins the cascade. |
This all depends on the new client-side extension system being implemented in #1373, so this issue is blocked by that. I suppose a new dependent plugin would be required for this as it wouldn't make sense in Image Prioritizer or Embed Optimizer. |
hey @westonruter |
@benniledl here is a breakdown: https://almanac.httparchive.org/en/2022/performance#fig-7 |
Does the element type (div or h1) really matter? As long as we get information from Optimization Detective about the LCP element and the font it uses, then that's all that's needed to preload the font file. |
I thought that the challenge lies in identifying which element is the LCP element. So I thought: If statistics indicate that text elements, like headers, are frequently the LCP element, we could simplify by not determining the lcp but just preloading the font used for |
Normally, yes. But with Optimization Detective that's not a problem. With it, we get the LCP element information when you visit a webpage, then we store that in the database and on the next page load we retrieve it from there to add the preloading. That's how Embed Optimizer and Image Prioritizer work. Hope that makes sense.
@westonruter we could perhaps rename it to "Assets Prioritizer" or "Media Prioritizer" |
Right, Optimization Detective leverages web-vitals.js which tells us what the LCP element is. For example, this patch logs out the LCP element: diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js
index 9445ffbe..f468196d 100644
--- a/plugins/optimization-detective/detect.js
+++ b/plugins/optimization-detective/detect.js
@@ -437,6 +437,9 @@ export default async function detect( {
await new Promise( ( resolve ) => {
onLCP(
( /** @type LCPMetric */ metric ) => {
+ for ( const entry of metric.entries ) {
+ console.log( 'LCP candidate element:', entry.element );
+ }
lcpMetricCandidates.push( metric );
resolve();
},
I think perhaps a new "Text Optimizer" plugin is warranted for this actually. Optimizing text is a very different problem space than optimizing images (and video), where there will need to be more client-side logic to sniff out the font being used for the LCP element. In this way it is similar to how Embed Optimizer is also a separate plugin which is tailored for the needs of embeds (e.g. adding a As for how to get this started with implementation: I think the [
{
"family": "Inter var",
"src": "https://example.com/wp-content/themes/twentytwenty/assets/fonts/inter/Inter-upright-var.woff2",
"format": "woff2"
}
] Then a This script module would then add its own performance/plugins/optimization-detective/detect.js Lines 364 to 367 in 6bb8405
This extension script module would then need to do some CSS legwork to:
When the extension module script's So then the font data should be stored in the URL Metrics. Then we need to actually optimize the page by adding the font preload link. This would be handled differently than how other optimizations have been applied with Optimization Detective, since they have relied on tag visitor callbacks to apply optimizations to specific tags located in the page In particular, registered tag visitors mark potential elements to be optimized so that their XPaths will be stored in URL Metrics in order to be re-located during optimization. For optimizing LCP text, however, we don't really need a tag visitor at all. We just need the HTML Tag Processor instance to be passed to an object prior to it being serialized so that the necessary font preload link can be inserted. For example: --- a/plugins/optimization-detective/optimization.php
+++ b/plugins/optimization-detective/optimization.php
@@ -235,6 +235,15 @@ function od_optimize_template_output_buffer( string $buffer ): string {
}
} while ( $processor->next_open_tag() );
+ /**
+ * Fires after the document has been iterated over and each tag has been visited by the registered tag visitors.
+ *
+ * @since n.e.x.t
+ *
+ * @param OD_Tag_Visitor_Context $tag_visitor_context
+ */
+ do_action( 'od_document_tags_visited', $tag_visitor_context );
+
// Send any preload links in a Link response header and in a LINK tag injected at the end of the HEAD.
if ( count( $link_collection ) > 0 ) {
$response_header_links = $link_collection->get_response_header(); (Aside: The With such a
I believe this will get us to automatically preloading the fonts for LCP text. Important: Since the client is discovering the font URL, it will be critical to ensure that when the URL Metric is submitted that the font URL is valid. A malicious user could attempt to send a URL Metric that points to a bogus font file, for example a URL on some untrusted domain. It will be critical to validate the URL. If the font file is on the same domain of the site, a file exists check could be used. Otherwise, if the font file is on another domain than there should probably be an allowlist of origins that fonts can come from (e.g. Google Fonts). There is also a risk that malicious actors could add preload links for many fonts that aren't even used on the page. I don't think there's anything we can do to prevent this. It's a similar issue to #1584 where if we store the URL of a CSS background image for prioritization in URL Metrics, there is the potential for abuse. This is why Image Prioritizer has until now only stored the XPath for the image element to prioritize, and then the actual URL to preload is computed at runtime from the document. However, for background images (or fonts) coming from stylesheets, it is not feasible to do such runtime processing. |
I did some benchmarking to see what impact preloading the font would have. I used Local to create a vanilla site with the Twenty Twenty theme active. Then, in order to ensure that the H1 is the LCP element, I created a post with a long title that had text spanning 4 lines: I then added the following plugin: <?php
/**
* Plugin Name: Preload Font
*/
add_action( 'wp_head', static function () {
if ( isset( $_GET['disable_font_preload'] ) ) {
return;
}
?>
<link rel="preload" as="font" type="font/woff2" href="http://localhost:10058/wp-content/themes/twentytwenty/assets/fonts/inter/Inter-upright-var.woff2" fetchpriority="high" crossorigin="anonymous">
<?php
}, 0 ); I then created a
Then I used npm run research benchmark-web-vitals -- --file=font-preload-urls.txt -n 10 -w "360x640" Unfortunately, the preload link actually resulted in worse performance, which is surprising to me:
Here LCP is 7.67% slower. Why? |
Feature Description
When the LCP element is text, the loading of the font being used should be prioritized. For example, on one of my blog posts (using the Twenty Twenty theme), the LCP element is an
h1
. It has afont-family
style of:The
Inter var
font is loaded via this stylesheet:The
@font-face
rule is:The
font-inter.css
stylesheet is already loaded with highest priority, but the font file is not in the critical path so it is not discovered until after the critical CSS is parsed:To improve performance, this font file should be getting loaded sooner by adding this link:
This allows the font file to start loading the same time as the
font-inter.css
stylesheet:And this will improve LCP.
Note that
h1
is LCP element 5% of the time on mobile, withh2
andh3
being 2% and 1% respectively. Thep
element is the LCP element 9% off the time on mobile.The text was updated successfully, but these errors were encountered: