From 77132df0605f474ed3910f48e8e677e5ebd52683 Mon Sep 17 00:00:00 2001 From: Rick Viscomi Date: Tue, 10 Dec 2024 12:07:46 -0500 Subject: [PATCH] Performance 2024: image prioritization and discoverability (#3960) * Update lcp_preload_discoverable.sql * Create lcp_async_fetchpriority.sql * Add prioritization and discoverability stats * % with unsized images * Update src/content/en/2024/performance.md Co-authored-by: Barry Pollard * "lazy loading" * minor editing * note about native lazy loading discoverability --------- Co-authored-by: Barry Pollard --- .../performance/lcp_async_fetchpriority.sql | 107 ++++++++++++++++++ .../performance/lcp_preload_discoverable.sql | 8 +- src/content/en/2024/performance.md | 79 ++++++++++--- 3 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 sql/2024/performance/lcp_async_fetchpriority.sql diff --git a/sql/2024/performance/lcp_async_fetchpriority.sql b/sql/2024/performance/lcp_async_fetchpriority.sql new file mode 100644 index 00000000000..c06ebaa96ce --- /dev/null +++ b/sql/2024/performance/lcp_async_fetchpriority.sql @@ -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 diff --git a/sql/2024/performance/lcp_preload_discoverable.sql b/sql/2024/performance/lcp_preload_discoverable.sql index 3dd5699973a..45a20d7208e 100644 --- a/sql/2024/performance/lcp_preload_discoverable.sql +++ b/sql/2024/performance/lcp_preload_discoverable.sql @@ -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 ) diff --git a/src/content/en/2024/performance.md b/src/content/en/2024/performance.md index 2394fcb751d..9fcead3b133 100644 --- a/src/content/en/2024/performance.md +++ b/src/content/en/2024/performance.md @@ -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. @@ -35,7 +35,7 @@ What's new this year? - [Long Animation Frames (LoAF)](https://developer.chrome.com/docs/web-platform/long-animation-frames) data is available for the first time, providing new insights into the reasons for poor INP. - As of this year, the Performance chapter also includes an analysis of the data for secondary pages in addition to home pages. This allows us to compare the home page with the secondary page performance. -### Notes on Data Sources +### Notes on data sources The HTTP Archive contains only lab performance data. In other words, it is data from a single website load event. This is useful but limited if we want to understand how users experience performance. @@ -105,7 +105,7 @@ As of this year, secondary pages are available to compare with home page data. Secondary pages demonstrate significantly better CWV results than home pages. The percentage of the desktop secondary pages with good CWV is by 14 percentage points better than for home pages. For mobile websites, the difference is 13 percentage points. By looking at CWV data only, it is hard to identify what kind of performance experience is better. We will explore these aspects—layout shift, loading, and interactivity—in the corresponding sections. -## Loading Speed +## Loading speed People often refer to website loading speed as a single metric, but in fact, the loading experience is a multi-stage process. No single metric fully captures all aspects of what makes up loading speed. Every stage has an impact on the speed of a website. @@ -194,7 +194,7 @@ Several stages of processing must occur before the LCP element can be fully rend - **Resource Load Duration** which measures how long it takes to load the LCP resource; this stage is also 0 millisecond if no resource is needed. - **Element Render Delay** which is the time between when the resource finished loading and the LCP element finished rendering. -In the article [Common Misconceptions About How to Optimize LCP](https://web.dev/blog/common-misconceptions-lcp#lcp_sub-part_breakdown), Brendan Kenny analyzed a breakdown of LCP sub-parts using recent CruX data. +In the article [Common Misconceptions About How to Optimize LCP](https://web.dev/blog/common-misconceptions-lcp#lcp_sub-part_breakdown), Brendan Kenny analyzed a breakdown of LCP sub-parts using recent CrUX data. {{ figure_markup( image="median-subpart-p75s.png", @@ -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. + +#### 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.", @@ -243,6 +258,10 @@ 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. + +Note that the `src` attribute of an LCP image wth `loading=lazy` is technically set and therefore discoverable in the static HTML, so we don't count it towards the static discoverability figure in the previous section. However, natively lazy-loaded images absolutely do contribute to resource load delays, albeit in a slightly different way than an image whose source is set by CSS or JavaScript, as we'll explore next. + #### CSS background images {{ figure_markup( @@ -275,6 +294,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 core support 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. @@ -319,7 +355,7 @@ You can reduce image sizes through resizing dimensions and increasing compressio JPG and PNG still have the highest proportion of adoption at 87% combined, however WebP and AVIF formats are both increasing in adoption. In comparison to 2022, WebP image format usage increased from [4%](../2022/performance#lcp-format) to 7%. Also, AVIF usage increased slightly from 0.1% to 0.3%. According to Baseline, AVIF format is newly available across major browsers, so we expect to see higher adoption in the future. -### Loading Speed Conclusions +### Loading speed conclusions - The percentage of websites with good FCP and LCP has improved, though TTFB showed no significant change. - One cause for slow LCP is lazy-loading the LCP element. Usage of this antipattern has decreased, but 15% of websites still fail this test and could benefit from removing lazy-loading for their LCP elements. @@ -391,7 +427,7 @@ This could be because the most visited websites often have more user interaction Unlike other performance metrics like FCP and LCP, the percentage of secondary pages with good INP does not differ from the home page results. This is likely because INP isn't as impacted by caching as loading speed is. -#### INP Subparts +#### INP sub-parts Interaction to Next Paint metric can be broken down into three key sub-parts: @@ -403,8 +439,8 @@ To optimize your website's interactivity, it's important to identify the duratio {{ figure_markup( image="interaction-to-next-paint-subparts-rum-vision.png", - caption="INP Subparts by percentile.", - description="Bar chart showing the distribution of INP subparts in milliseconds (ms) by percentile. At the 10th percentile, all subparts (input delay, processing time, and presentation delay) are minimal. At the 25th percentile, the values slightly increase but remain below 10 milliseconds. At the 50th percentile, input delay and processing time stay modest, while presentation delay reaches around 20 milliseconds. At the 75th percentile, input delay increases to around 50 milliseconds, with processing time and presentation delay also rising. At the 90th percentile, input delay reaches around 150 milliseconds, and both processing time and presentation delay exceed 100 milliseconds.", + caption="INP sub-parts by percentile.", + description="Bar chart showing the distribution of INP sub-parts in milliseconds (ms) by percentile. At the 10th percentile, all sub-parts (input delay, processing time, and presentation delay) are minimal. At the 25th percentile, the values slightly increase but remain below 10 milliseconds. At the 50th percentile, input delay and processing time stay modest, while presentation delay reaches around 20 milliseconds. At the 75th percentile, input delay increases to around 50 milliseconds, with processing time and presentation delay also rising. At the 90th percentile, input delay reaches around 150 milliseconds, and both processing time and presentation delay exceed 100 milliseconds.", chart_url="https://docs.google.com/spreadsheets/d/e/2PACX-1vRiPhLGlGUxomTx_5nC9ahQDRxZBmJXMT3Q0Z2z4Y2pPVqC9kzjsUjRk4hz-JZzaPBjVxyaf7Gtqh93/pubchart?oid=226800794&format=interactive", sheets_gid="731456372", ) @@ -412,7 +448,7 @@ To optimize your website's interactivity, it's important to identify the duratio The INP sub-part duration distribution data from RUMvision shows that presentation delay (36 milliseconds) contributes the most to the median INP. As percentiles increase, input delay and processing time become longer. At the 75th percentile, input delay reaches 37 milliseconds and processing delay 56 milliseconds. By the 90th percentile, input delay jumps to 155 milliseconds, which makes it the biggest contributor to poor INP. One way to optimize input delay is by avoiding long tasks, which we explore in the Long Tasks section. -### Long Tasks +### Long tasks One of the sub-parts of INP is input delay, which can be longer than it should be due to various factors, including long tasks. [A task](https://web.dev/articles/optimize-long-tasks#what-is-task) is a discrete unit of work that the browser executes, and JavaScript is often the largest source of tasks. When a task exceeds 50 milliseconds, it is considered a long task. These long tasks can cause delays in responding to user interactions, directly affecting interactivity performance. @@ -431,7 +467,7 @@ The task duration distribution shows a median task duration of 90 milliseconds f Task duration data was retrieved using the Long Tasks API, which provides some useful data about performance issues, but it has limitations when it comes to accurately measuring sluggishness. It only identifies when a long task occurs and how long it lasts. It might overlook essential tasks such as rendering. Due to these limitations, we will explore the Long Animation Frames API in the next section, which offers more detailed insights. -#### Long Animations Frames +#### Long animations frames [Long Animation Frames (LoAF)](https://developer.chrome.com/docs/web-platform/long-animation-frames) are a performance timeline entry for identifying sluggishness and poor INP by tracking when work and rendering block the main thread. LoAF tracks animation frames instead of individual tasks like the Long Tasks API. A long animation frame is when a rendering update is delayed beyond 50 milliseconds (the same as the threshold for the Long Tasks API). It helps to find scripts that cause INP performance bottlenecks. This data allows us to analyze INP performance based on the categories of scripts responsible for LoAF. @@ -484,7 +520,7 @@ The median TBT on mobile is 1,209 milliseconds, which is 6 times higher than the With TBT being caused by long tasks it is not surprising to notice the same trend per pecentiles as well as similar trend in gap between mobile and desktop in the two metrics results. It is also important to note that high TBT can be contributing to the input delay part of the INP, negatively imacting the overall INP score. -### Interactivity Conclusion +### Interactivity conclusion The main takeaways of the interactivity results are: @@ -493,7 +529,7 @@ The main takeaways of the interactivity results are: - INP can be divided into three sub-parts: Input Delay, Processing Time, and Presentation Delay. Presentation Delay has the biggest share of the median INP in RUMvisions's data. - Scripts from user behavior tracking, consent provider, and CDN categories are the main contributors to poor INP scores. -## Visual Stability +## Visual stability Visual stability on a website refers to the consistency and predictability of visual elements as the page loads and users interact with it. A visually stable website ensures that content does not unexpectedly shift, move, or change layout, which can disrupt the user experience. These shifts often happen due to assets without specified dimensions (images and videos), third-party ads, heavy fonts, etc. The primary metric for measuring visual stability is [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls). @@ -574,6 +610,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.", @@ -659,7 +706,7 @@ Another cause of unexpected shifts can be [non-composited](https://developer.chr 39% of mobile pages and 42% of desktop pages still use non-composited animations, which is a very slight increase from 38% for mobile and 41% for desktop in the analysis from 2022. -### Visual Stability Conclusion +### Visual stability conclusion Visual stability of the site can have a big influence on the user experience of the page. Having text shifting around while reading or a button we were just about to click disappear from the viewport can lead to user frustration. The good news is that Cumulative Layout Shift (CLS) continued to improve in 2024. That indicates that more and more website owners are adopting good practices such as sizing images and preserving space for dynamic content, as well as optimizing for bfcache eligibility to benefit from this browser feature.