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

Performance 2024: image prioritization and discoverability #3960

Merged
merged 8 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions sql/2024/performance/lcp_async_fetchpriority.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
CREATE TEMP FUNCTION getLoadingAttr(attributes STRING) RETURNS STRING LANGUAGE js AS '''
try {
const data = JSON.parse(attributes);
const loadingAttr = data.find(attr => attr["name"] === "loading")
return loadingAttr.value
} catch (e) {
return "";
}
''';

CREATE TEMP FUNCTION getDecodingAttr(attributes STRING) RETURNS STRING LANGUAGE js AS '''
try {
const data = JSON.parse(attributes);
const decodingAttr = data.find(attr => attr["name"] === "decoding")
return decodingAttr.value
} catch (e) {
return "";
}
''';

CREATE TEMP FUNCTION getFetchPriorityAttr(attributes STRING) RETURNS STRING LANGUAGE js AS '''
try {
const data = JSON.parse(attributes);
const fetchPriorityAttr = data.find(attr => attr["name"] === "fetchpriority")
return fetchPriorityAttr.value
} catch (e) {
return "";
}
''';

CREATE TEMP FUNCTION getLoadingClasses(attributes STRING) RETURNS STRING LANGUAGE js AS '''
try {
const data = JSON.parse(attributes);
const classes = data.find(attr => attr["name"] === "class").value
if (classes.indexOf('lazyload') !== -1) {
return classes
} else {
return ""
}
} catch (e) {
return "";
}
''';

WITH
lcp_stats AS (
SELECT
client,
page AS url,
JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.nodeName') AS nodeName,
JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.url') AS elementUrl,
CAST(JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.size') AS INT64) AS size,
CAST(JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.loadTime') AS FLOAT64) AS loadTime,
CAST(JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.startTime') AS FLOAT64) AS startTime,
CAST(JSON_EXTRACT_SCALAR(custom_metrics.performance, '$.lcp_elem_stats.renderTime') AS FLOAT64) AS renderTime,
JSON_EXTRACT(custom_metrics.performance, '$.lcp_elem_stats.attributes') AS attributes,
getLoadingAttr(TO_JSON_STRING(custom_metrics.performance.lcp_elem_stats.attributes)) AS loading,
getDecodingAttr(TO_JSON_STRING(custom_metrics.performance.lcp_elem_stats.attributes)) AS decoding,
getLoadingClasses(TO_JSON_STRING(custom_metrics.performance.lcp_elem_stats.attributes)) AS classWithLazyload,
getFetchPriorityAttr(TO_JSON_STRING(custom_metrics.performance.lcp_elem_stats.attributes)) AS fetchPriority
FROM
`httparchive.crawl.pages`
WHERE
date = '2024-11-01' AND
is_root_page
)

SELECT
client,
nodeName,
COUNT(DISTINCT url) AS pages,
ANY_VALUE(total) AS total,
COUNT(DISTINCT url) / ANY_VALUE(total) AS pct,
COUNTIF(elementUrl != '') AS haveImages,
COUNTIF(elementUrl != '') / COUNT(DISTINCT url) AS pct_haveImages,
COUNTIF(loading = 'eager') AS native_eagerload,
COUNTIF(loading = 'lazy') AS native_lazyload,
COUNTIF(classWithLazyload != '') AS lazyload_class,
COUNTIF(classWithLazyload != '' OR loading = 'lazy') AS probably_lazyLoaded,
COUNTIF(classWithLazyload != '' OR loading = 'lazy') / COUNT(DISTINCT url) AS pct_prob_lazyloaded,
COUNTIF(decoding = 'async') AS async_decoding,
COUNTIF(decoding = 'sync') AS sync_decoding,
COUNTIF(decoding = 'auto') AS auto_decoding,
COUNTIF(fetchPriority = 'low') AS priority_low,
COUNTIF(fetchPriority = 'high') AS priority_high
FROM
lcp_stats
JOIN (
SELECT
client,
COUNT(0) AS total
FROM
`httparchive.crawl.pages`
WHERE
date = '2024-11-01' AND
is_root_page
GROUP BY
client)
USING
(client)
GROUP BY
client,
nodeName
HAVING
pages > 1000
ORDER BY
pct DESC
8 changes: 4 additions & 4 deletions sql/2024/performance/lcp_preload_discoverable.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
WITH lcp AS (
SELECT
client,
JSON_VALUE(custom_metrics, '$.performance.is_lcp_statically_discoverable') = 'true' AS discoverable,
JSON_VALUE(custom_metrics, '$.performance.is_lcp_preloaded') = 'true' AS preloaded
JSON_VALUE(custom_metrics.performance, '$.is_lcp_statically_discoverable') = 'true' AS discoverable,
JSON_VALUE(custom_metrics.performance, '$.is_lcp_preloaded') = 'true' AS preloaded
FROM
`httparchive.all.pages`
`httparchive.crawl.pages`
WHERE
date = '2024-06-01' AND
date = '2024-11-01' AND
is_root_page
)

Expand Down
53 changes: 49 additions & 4 deletions src/content/en/2024/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ results: https://docs.google.com/spreadsheets/d/15038wEIoqY53Y_kR8U6QWM-PBO31ZyS
featured_quote: Web performance is improving across loading times, interactivity, and visual stability. However, the gap between mobile and desktop experiences remains significant.
featured_stat_1: 43%
featured_stat_label_1: of mobile websites have good CWV scores when measured with INP, which is 5% lower than when measured with FID.
featured_stat_2: 15%
featured_stat_2: 16%
featured_stat_label_2: of websites still use unnecessary lazy-loading on LCP elements.
featured_stat_3: 13%
featured_stat_label_3: the percentage by which good CWV scores are higher on secondary pages compared to home pages for mobile websites.
Expand Down Expand Up @@ -226,11 +226,26 @@ Typically, LCP element rendering takes a long time if the LCP element hasn't bee

It's interesting to observe the different LCP challenges that websites across various datasets face. While an average website from the CrUX dataset struggles with image load delay, websites from the RUMvision dataset often face rendering delay issues. Nevertheless, all websites can benefit from using performance monitoring tools with Real User Monitoring (RUM), as these tools provide deeper insights into the performance issues experienced by real users.

#### LCP lazy-loading
#### LCP static discoverability

One of the most effective ways to optimize the LCP resource load delay is to ensure the resource can be discovered as early as possible. If you make the resource discoverable in the initial HTML document, it enables the LCP resource to begin downloading sooner.

{{ figure_markup(
caption="The percent of mobile pages on which the LCP element was not statically discoverable.",
content="35%",
classes="big-number",
sheets_gid="200850285",
sql_file="lcp_preload_discoverable.sql"
)
}}

One of the ways to optimize the LCP resource load delay is to ensure the resource can be discovered as early as possible. If you make the resource discoverable in the initial HTML document, it enables the LCP resource to begin downloading sooner. A big obstacle to LCP resource discoverability is lazy loading of the LCP resource.
Unfortunately, 35% of mobile websites do not have an LCP element that is statically discoverable in the document. While this is a slight improvement over the 39% we saw in 2022, it's still a significant blocker of LCP performance.

Overall, lazy-loading images is a helpful performance technique that should be used to postpone loading of non-critical resources until they are near the viewport. However, using lazy-loading on the LCP image will delay the browser from loading it quickly. That is why lazy-loading should not be used on LCP elements. In this section, we explore how many sites use this performance anti-pattern.
As we'll explore in the following sections, there are three primary ways that websites prevent their LCP resources from being statically discoverable: lazy-loading, CSS background images, and client-side rendering.
Copy link
Contributor

@kevinfarrugia kevinfarrugia Dec 10, 2024

Choose a reason for hiding this comment

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

If an LCP image is lazy loaded (natively) then it is still statically discoverable. Only custom approaches (6.7% of sites) are included in the 35%. Might be worth clarifying.

Copy link
Member

Choose a reason for hiding this comment

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

If an LCP image is lazy loaded (natively) then it is still statically discoverable

Effectively not. It's not discoverable by the preload scanner, and needs CSS to run layout to know if it's in viewport. Which effectively means it's not statically discoverable (i.e. it's the same as if it was hidden in CSS background image, or within JS that needs to run).

Copy link
Contributor

@kevinfarrugia kevinfarrugia Dec 10, 2024

Choose a reason for hiding this comment

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

Fair enough, but the custom metric definition of is_statically_discoverable doesn't take this into consideration.

The image is considered statically discoverable if it is preloaded or available in the image src or srcset:

      const rawLcpElement = Array.from(rawDoc.querySelectorAll('picture source, img, svg image')).find(i => {
        let src = i.src;
        if (i.hasAttribute('srcset')) {
            src = splitSrcSet(i.srcset).find(src => src == lcpUrl);
        } else if (i.hasAttribute('href')) {
            src = i.getAttribute('href');
        }

        return new URL(src, location.href).href == lcpUrl;
    });

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point @kevinfarrugia, I think it's at least worth mentioning that native lazy loading isn't included in the static discoverability stat. We might want to consider looking at a different high-level metric in the future that captures all of the causes of resource load delay.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated PTAL

rviscomi marked this conversation as resolved.
Show resolved Hide resolved

#### LCP lazy-loading

A major obstacle to LCP resource discoverability is lazy-loading of the LCP resource. Overall, lazy-loading images is a helpful performance technique that should be used to postpone loading of non-critical resources until they are near the viewport. However, using lazy-loading on the LCP image will delay the browser from loading it quickly. That is why lazy-loading should not be used on LCP elements.

{{ figure_markup(
caption="The percent of mobile pages having image-based LCP that use native or custom lazy-loading on it.",
Expand All @@ -243,6 +258,8 @@ Overall, lazy-loading images is a helpful performance technique that should be u

The good news is that in 2024, fewer websites are using this performance anti-pattern. In 2022, 18% of mobile websites were lazy-loading their LCP images. By 2024, this decreased to 16%.

In terms of the specific lazy-loading technique used, 9.5% of mobile websites natively lazy-load their LCP images with the `loading=lazy` attribute. This is very similar to the 9.8% of sites we saw in 2022. However, the biggest improvement came from custom approaches. This year we see 6.7% of mobile websites using a custom approach, for example hiding the LCP image source behind the `data-src` attribute, which is down from 8.8% in 2022.

#### CSS background images

{{ figure_markup(
Expand Down Expand Up @@ -275,6 +292,23 @@ The chart below illustrates the distribution of client-side generated content. I

The percentage of pages with good LCP stays at approximately 60% for mobile devices until the amount of client-side generated content reaches 70%. After this threshold, the percentage of websites with good LCP starts to drop at a faster rate until ending at 40%. This suggests that a combination of server- and client-side generated content doesn't significantly impact how fast the LCP element gets rendered. However, fully rendering a website on the client side has a significantly negative impact on LCP.

#### LCP prioritization

Another one of the most effective ways to optimize the loading delay of LCP images is to declaratively prioritize them, using the `fetchpriority=high` attribute. Even if the LCP resource is statically discoverable by the browser's preload scanner, it might still not start loading immediately if there are other higher priority resources in line. Images are typically not considered high priority resources, so by providing this hint to the browser, it can adjust the LCP resource's priority accordingly, loading it sooner and reducing its load delay phase.

{{ figure_markup(
caption="The percent of mobile pages that use `fetchpriority=high` on their LCP image.",
content="15%",
classes="big-number",
sheets_gid="731441901",
sql_file="lcp_async_fetchpriority.sql"
)
}}

Adoption of LCP image prioritization skyrocketed to 15% of mobile websites in 2024, up from just 0.03% in 2022! This massive leap is thanks in large part to WordPress implementing <a hreflang="en" href="https://make.wordpress.org/core/2023/07/13/image-performance-enhancements-in-wordpress-6-3/">core support</a> for `fetchpriority` in 2023.

As amazing as it is to see such rapid growth, there is still significant room for more sites to take advantage of this impactful one-line optimization.

#### LCP size

The CrUX and RUMvision data on [LCP sub-parts](#lcp-sub-parts) showed that resource load duration is rarely the main bottleneck for a slow LCP. However, it is still valuable to analyze the key optimization factors, such as the size and format of the LCP resource.
Expand Down Expand Up @@ -574,6 +608,17 @@ The following best practices allow you to reduce, or even completely avoid CLS.

One of the most common reasons for unexpected layout shifts is not preserving space for assets or incoming dynamic content. For example, adding `width` and `height` attributes on images is one of the easiest ways to preserve space and avoid shifts.

{{ figure_markup(
content="66%",
caption="The percent of mobile pages that fail to set explicit dimensions on at least one image.",
classes="big-number",
sheets_gid="1674162543",
sql_file="cls_unsized_images.sql"
)
}}

66% of mobile pages have at least one unsized image, which is an improvement from 72% in 2022.

{{ figure_markup(
image="unsized-images-amount.png",
caption="The number of unsized images per page.",
Expand Down