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

Improve rendering performance with content-visibility #1308

Open
westonruter opened this issue Jun 22, 2024 · 16 comments
Open

Improve rendering performance with content-visibility #1308

westonruter opened this issue Jun 22, 2024 · 16 comments
Assignees
Labels
[Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Feature A new feature within an existing module

Comments

@westonruter
Copy link
Member

westonruter commented Jun 22, 2024

Feature Description

With the URL metrics gathered by Optimization Detective, it should be possible to automatically apply content-visibility to areas of the page to delay rendering them until they are in the viewport. This has the opportunity to improve INP.

See:

@westonruter westonruter added [Type] Feature A new feature within an existing module [Plugin] Optimization Detective Issues for the Optimization Detective plugin labels Jun 22, 2024
@westonruter westonruter changed the title Improve rendering performance with content-visibility and CSS containment Improve rendering performance with content-visibilit Jun 22, 2024
@westonruter westonruter changed the title Improve rendering performance with content-visibilit Improve rendering performance with content-visibility Jun 22, 2024
@github-project-automation github-project-automation bot moved this to Not Started/Backlog 📆 in WP Performance 2024 Sep 30, 2024
@westonruter
Copy link
Member Author

Content-Visibility is now baseline!

@tunyk
Copy link

tunyk commented Dec 14, 2024

Please add this CSS at least for basic WordPress themes

@westonruter
Copy link
Member Author

@tunyk How do you see it being used in themes?

I believe this would be most effective when content-visibility is applied to each root block element in the post content or else for each post's container element in the loop on an archive page. In order to do this, it is necessary to have the height of the element computed on the various breakpoints. This is something that Optimization Detective can provide. I think this would be independent of any theme-specific styling.

@tunyk
Copy link

tunyk commented Dec 16, 2024

I agree, in the context of standard themes. But if we are talking about all themes, then you should take into account that many of them use infinite scroll and Ajax.

@westonruter
Copy link
Member Author

@tunyk So how do you see this being applied? In order to apply content-visibility you need to know what the intrinsic dimensions are for the element that is being hidden prior being shown to specify in the contain-intrinsic-size style, right? I'm having a hard time envisioning how apply content-visibility in the context of infinite scroll. Also, isn't infinite scroll somewhat of an alternative to content-visibility in the first place? With content-visibility you can have a huge document and not hurt rendering performance. With infinite scroll you start with a small document and then add to it as you scroll. So both are effectively two ways to solve the same problem.

@tunyk
Copy link

tunyk commented Dec 16, 2024

There is no clear-cut solution here. Theoretically (depending on the implementation of infinite scrolling), the Speculative API may also be involved here.

@westonruter
Copy link
Member Author

For reference, I put together an example of the impact that content-visibility can have when applied to the container element of a post in The Loop on an archive page:

Disabled: PageSpeed Insights
Enabled: PageSpeed Insights

For both mobile and desktop, the LCP is degraded when using content-visibility for some reason.

@westonruter
Copy link
Member Author

Oh, the degraded LCP may be because I included content-visibility for the initial post as well:

#post-1241 {
    content-visibility: auto;
    contain-intrinsic-size: auto 846px;
}

This results in lazy rendering of the first (sticky) post which is in the initial viewport. Lazy loading (or rendering) anything in the initial viewport is a performance anti-pattern. I'll remove that and re-test.

@westonruter
Copy link
Member Author

westonruter commented Dec 20, 2024

OK, this is looking better:

Device Without content-visibility With content-visibility
Mobile Mobile without CV Mobile with CV
Desktop Desktop without CV Desktop with CV

Look at those total reductions in Total Blocking Time. They're about cut in half.

I'm running benchmarks now to get the LCP-TTFB...

@westonruter
Copy link
Member Author

I ran the following script to get the median metrics of 50 responses each with and without content-visibility on both mobile and desktop:

number=50
npm run research -- benchmark-web-vitals -u "https://content-visibility-perf-test.instawp.xyz/" -n $number -o csv -e "Moto G4" | tee mobile-without-cv.csv
npm run research -- benchmark-web-vitals -u "https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1" -n $number -o csv -e "Moto G4" | tee mobile-with-cv.csv
npm run research -- benchmark-web-vitals -u "https://content-visibility-perf-test.instawp.xyz/" -n $number -o csv -w "desktop" | tee desktop-without-cv.csv
npm run research -- benchmark-web-vitals -u "https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1" -n $number -o csv -w "desktop" | tee desktop-with-cv.csv
Raw output
for file in $(ls -r *.csv); do echo "# $file"; cat $file; echo; done
# mobile-without-cv.csv

> research
> ./cli/run.mjs benchmark-web-vitals -u https://content-visibility-perf-test.instawp.xyz/ -n 50 -o csv -e Moto G4

URL,https://content-visibility-perf-test.instawp.xyz/
Success Rate,100%
FCP (median),446.65
LCP (median),446.65
TTFB (median),310
LCP-TTFB (median),130.5


# mobile-with-cv.csv

> research
> ./cli/run.mjs benchmark-web-vitals -u https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1 -n 50 -o csv -e Moto G4

URL,https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1
Success Rate,100%
FCP (median),414.25
LCP (median),414.25
TTFB (median),323.9
LCP-TTFB (median),89.75


# desktop-without-cv.csv

> research
> ./cli/run.mjs benchmark-web-vitals -u https://content-visibility-perf-test.instawp.xyz/ -n 50 -o csv -w desktop

URL,https://content-visibility-perf-test.instawp.xyz/
Success Rate,100%
FCP (median),482.85
LCP (median),482.85
TTFB (median),359.15
LCP-TTFB (median),121.6


# desktop-with-cv.csv

> research
> ./cli/run.mjs benchmark-web-vitals -u https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1 -n 50 -o csv -w desktop

URL,https://content-visibility-perf-test.instawp.xyz/?try-content-visibility=1
Success Rate,100%
FCP (median),486.85
LCP (median),486.85
TTFB (median),375.55
LCP-TTFB (median),107.15

Reduction in LCP-TTFB:

  Without CV With CV Diff %
Mobile 130.5 89.75 -31.23%
Desktop 121.6 107.15 -11.88%

Now we're getting somewhere!

@westonruter
Copy link
Member Author

westonruter commented Dec 21, 2024

Granted, the test page is much heavier than the average webpage (er, >40x larger than the median webpage). But this is the first time I've seen proof of content-visibility actually improving LCP.

@tunyk
Copy link

tunyk commented Dec 21, 2024

Could you please update your WordPress and theme version? That way the comparison will be more accurate.

@westonruter
Copy link
Member Author

I just updated them. However, I don't see how this will make the comparison more accurate. It's still just testing the impact of content-visibility on the same page otherwise.

@westonruter
Copy link
Member Author

I've created a POC plugin which implements content-visibility:auto on top of Optimization Detective: https://github.com/westonruter/od-content-visibility

The plugin collects the initial height of container elements for posts in The Loop for a given responsive breakpoint width (mobile, phablet, tablet, and desktop). Then for subsequent page loads it adds style rules with media queries which apply CV to the elements. For example:

@media screen and (max-width: 480px) { 
    #post-27 { content-visibility: auto; contain-intrinsic-size: auto 876.41925048828px; }
}
@media screen and (min-width: 783px) { 
    #post-27 { content-visibility: auto; contain-intrinsic-size: auto 966.296875px; } 
}

The initial height is captured via the boundingClientRect reported by the intersection observer. However, the height may change later, which means there could be some drift in the hidden height and the visible height. To account for this, the plugin adds a contentvisibilityautostatechange event handler so that whenever such an element is shown, it captures the actual height and then stores that in the URL Metric, rather than the initially-hidden element's height. Lastly, there is a complication with the URL Metrics in that if users repeatedly do not scroll down the page when URL Metrics are collected, then it will end up being that no height will be stored for the CV-auto elements. This is because we only keep a sample size of URL Metrics. Therefore, in order to account for that, this plugin explores persisting the height in postmeta independent of what is currently stored in URL Metrics.

I did some benchmarking with this plugin on a vanilla install which just contained this plugin active along with the Optimization Detective dependency. The site has the Twenty Twenty-One theme active. There are 10 published posts with featured images and lorem ipsum. I tested the homepage which lists the 10 excerpts of these posts and their featured images. I created the test site in LocalWP.

See mobile viewport with first two posts visible

Image

I created a file called cv-urls.txt which contained a URL with the optimizations disabled and another with the optimizations enabled:

http://localhost:10013/?optimization_detective_disabled=1
http://localhost:10013/

I then created a script which used the benchmark-web-vitals command to do 100 iterations of the test page with/without the optimizations on both desktop and mobile:

#!/bin/bash
number=100
npm --silent run research -- benchmark-web-vitals --file=cv-urls.txt -n $number -e "Moto G4" -o csv | tee mobile-cv.csv
npm --silent run research -- benchmark-web-vitals --file=cv-urls.txt -n $number -w desktop -o csv | tee desktop-cv.csv

Mobile results:

  Before After % Diff
FCP (median) 89.05 87.1 -2.19%
LCP (median) 89.05 87.1 -2.19%
TTFB (median) 32.5 39.25 +20.77%
LCP-TTFB (median) 54.55 50.05 -8.25%

Desktop results:

  Before After % Diff
FCP (median) 93.15 96.35 +3.44%
LCP (median) 93.15 96.35 +3.44%
TTFB (median) 31.05 36.8 +18.52%
LCP-TTFB (median) 61.8 58.65 -5.10%

In both cases, the TTFB is increased as expected, due to the extra server-side processing. In the case of mobile there was an overall decrease in LCP by 2.19% whereas on desktop the overall LCP increased by 3.44%. Nevertheless, in both mobile and desktop the LCP-TTFB decreased (improved) by 8.25% and 5.1%, respectively. This is the metric that matters when a full page cache is employed, as this would eliminate the TTFB penalty introduced by Optimization Detective.

I did my tests in the Linux environment of an HP Dragonfly Elite Chromebook.

@westonruter
Copy link
Member Author

I just discovered an interesting side effect of an element having a parent with content-visibility:auto. When the initial Intersection Observer runs and captures the boundingClientRect, all of the values are zero:

{
  "isLCP": false,
  "isLCPCandidate": false,
  "xpath": "/*[1][self::HTML]/*[2][self::BODY]/*[2][self::DIV]/*[2][self::MAIN]/*[2][self::DIV]/*[1][self::UL]/*[5][self::LI]/*[1][self::DIV]/*[2][self::DIV]/*[1][self::DIV]/*[1][self::FIGURE]/*[1][self::PICTURE]/*[2][self::IMG]",
  "intersectionRatio": 0,
  "intersectionRect": {
    "x": 0,
    "y": 0,
    "width": 0,
    "height": 0,
    "top": 0,
    "right": 0,
    "bottom": 0,
    "left": 0
  },
  "boundingClientRect": {
    "x": 0,
    "y": 0,
    "width": 0,
    "height": 0,
    "top": 0,
    "right": 0,
    "bottom": 0,
    "left": 0
  }
}

This makes sense, but it also causes problems because all of the images are then considered as if they are in the initial viewport by Image Prioritizer! Because of this, it is removing loading=lazy (which is not good) but at the same time it is adding adding fetchpriority=low (which is good):

// TODO: Take into account whether the element has the computed style of visibility:hidden, in such case it should also get fetchpriority=low.
// Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy.
$is_visible = $element_max_intersection_ratio > 0.0;
if ( true === $context->url_metric_group_collection->is_element_positioned_in_any_initial_viewport( $xpath ) ) {
if ( ! $is_visible ) {
// If an element is positioned in the initial viewport and yet it is it not visible, it may be
// located in a subsequent carousel slide or inside a hidden navigation menu which could be
// displayed at any time. Therefore, it should get fetchpriority=low so that any images which are
// visible can be loaded with a higher priority.
$updated_fetchpriority = 'low';
// Also prevent the image from being lazy-loaded (or eager-loaded) since it may be revealed at any
// time without the browser having any signal (e.g. user scrolling toward it) to start downloading.
$processor->remove_attribute( 'loading' );
} elseif ( $is_lazy_loaded ) {
// Otherwise, if the image is positioned inside any initial viewport then it should never get lazy-loaded.
$processor->remove_attribute( 'loading' );
}
} elseif ( ! $is_lazy_loaded && ! $is_visible ) {
// Otherwise, the element is not positioned in any initial viewport, so it should always get lazy-loaded.
// The `! $is_visible` condition should always evaluate to true since the intersectionRatio of an
// element positioned below the initial viewport should by definition never be visible.
$processor->set_attribute( 'loading', 'lazy' );
}

Optimization Detective itself may need to add a contentvisibilityautostatechange event handler to populate the boundingClientRect with the actual dimensions once it comes into view. Even so, it may never come into view, so it could still end up with zeros for all of the properties. This presents a difficulty for knowing whether to add loading=lazy or not, since we ideally want to add fetchpriority=low to IMG elements located somewhere in the initial viewport but hidden whereas if an IMG is outside the viewport and hidden, it gets loading=lazy.

@westonruter westonruter self-assigned this Dec 24, 2024
@westonruter westonruter moved this from Not Started/Backlog 📆 to In Progress 🚧 in WP Performance 2024 Dec 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Feature A new feature within an existing module
Projects
Status: In Progress 🚧
Development

No branches or pull requests

2 participants