WordPress / performance

Performance plugin from the WordPress Performance Group, which is a collection of standalone performance modules.
https://wordpress.org/plugins/performance-lab/
GNU General Public License v2.0
360 stars 98 forks source link

Prioritize loading fonts for textual LCP elements #1313

Open westonruter opened 3 months ago

westonruter commented 3 months ago

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 a font-family style of:

"Inter var", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif 

The Inter var font is loaded via this stylesheet:

<link rel="stylesheet" id="twentytwenty-fonts-css" href="https://weston.ruter.net/wp-content/themes/twentytwenty/assets/css/font-inter.css?ver=2.6" media="all">

The @font-face rule is:

@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");
}

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:

image

To improve performance, this font file should be getting loaded sooner by adding this link:

<link rel="preload" as="font" href="https://weston.ruter.net/wp-content/themes/twentytwenty/assets/fonts/inter/Inter-upright-var.woff2" fetchpriority="high">

This allows the font file to start loading the same time as the font-inter.css stylesheet:

image

And this will improve LCP.

Note that h1 is LCP element 5% of the time on mobile, with h2 and h3 being 2% and 1% respectively. The p element is the LCP element 9% off the time on mobile.

westonruter commented 4 days ago

I'm thinking about how this would be implemented in practice.

It seems it would rely on calling getComputedStyle() on the LCP text element to determine the current font-family. It would then need to get the first font in that list, and then iterate over all of the stylehseets in document.styleSheets and for all of the styleSheet.cssRules` inside of them, for example:

( 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-family name, but just vary in terms of the font-weight and font-style:

@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 rules altogether. The above are from Twenty Twenty's style.css. However, Twenty Twenty also includes the following in assets/css/font-inter.css:

@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 @font-face rules.

The last one encountered should be used since it wins the cascade.

westonruter commented 4 days ago

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.