Skip to content

Explore opportunities to use Optimization Detective to identify INP problems #1736

@westonruter

Description

@westonruter

Optimization Detective is currently being used to optimize LCP via Image Prioritizer and CLS via Embed Optimizer. However, for the other Core Web Vital, INP, it is not currently doing anything. This is somewhat expected given that there isn't really an element which can be optimized for INP issues like there can be for LCP and CLS. Nevertheless, Optimization Detective does provide a foundation for at least tracking INP issues:

  1. Initially, Optimization Detective would send the URL Metric as soon as the page has fully loaded and gone idle, but now it sends the URL Metric when the page is being closed. This means that all of the INP/LoAF data collected during the life of the page will be available for adding to a URL Metric for storage.
  2. As of Preload image URLs for LCP elements with external background images #1697 any Optimization Detective extension modules now have direct access to the onINP function as provided by Web Vitals.
  3. Optimization Detective extensions are able to extend the URL Metric schema via the od_url_metric_schema_root_additional_properties filter to add a new root property for something like inpData.
  4. Optimization Detective by default only stores a maximum of 12 URL Metrics per page (3 for each of the 4 viewport groups). This may not be a large enough sample size for collecting enough INP/LoAF data. To this end, there is a od_url_metric_stored action which is fired whenever a new URL Metric is stored. An extension could use this to grab the INP data out of the URL Metric and store it persistently in another location so it is not garbage-collected when a new URL Metric is collected. For example, the OD_URL_Metric_Store_Request_Context object passed to the od_url_metric_stored action includes not only the OD_URL_Metric instance but also the post ID for the od_url_metrics post type that the URL Metric is being stored in. An INP-focused extension could use this to persist the INP data in postmeta for that post, like od_inp_data:
add_action(
	'od_url_metric_stored',
	static function ( OD_URL_Metric_Store_Request_Context $context ) {
		$inp_data = $context->url_metric->get( 'inpData' );
		if ( null === $inp_data ) {
			return;
		}
		// Note: $unique is false to allow for multiple postmeta to be stored with the same key.
		add_post_meta( $context->post_id, 'od_inp_data', $inp_data, false );
	}
);

We can then get all of the INP data with:

$all_data = get_post_meta( $post_id, 'od_inp_data', false );

The question then remains: with this data in hand, what do we do with it?

Assuming we have something on the page that causes a long task:

add_action( 'wp_footer', static function () {
	?>
	<script>
		function slowThing(event) {
			const now = performance.now();
			while ( performance.now() - now < 300 ) {
				continue;
			}
			event.target.textContent = 'Clicked!';
		}
	</script>
	<button type="button" onclick="slowThing(event);">Click Me</button>
	<?php
} );

The INP data provided by Web Vitals includes:

{
    "name": "INP",
    "value": 312,
    "rating": "needs-improvement",
    "delta": 296,
    "entries": [
        {
            "name": "pointerup",
            "entryType": "event",
            "startTime": 2794.7000000029802,
            "duration": 312,
            "interactionId": 5364,
            "processingStart": 2796.900000002235,
            "processingEnd": 2797,
            "cancelable": true
        },
        {
            "name": "click",
            "entryType": "event",
            "startTime": 2794.7000000029802,
            "duration": 312,
            "interactionId": 5364,
            "processingStart": 2797.10000000149,
            "processingEnd": 3098.300000000745,
            "cancelable": true
        }
    ],
    "id": "v4-1734029682811-7860789630775",
    "navigationType": "reload",
    "attribution": {
        "interactionTarget": "html>body.home.blog.logged-in.admin-bar.no-customize-support.wp-embed-responsive>button",
        "interactionTargetElement": {},
        "interactionType": "pointer",
        "interactionTime": 2794.7000000029802,
        "nextPaintTime": 3106.7000000029802,
        "processedEventEntries": [
            {
                "name": "pointerup",
                "entryType": "event",
                "startTime": 2794.7000000029802,
                "duration": 312,
                "interactionId": 5364,
                "processingStart": 2796.900000002235,
                "processingEnd": 2797,
                "cancelable": true
            },
            {
                "name": "mouseup",
                "entryType": "event",
                "startTime": 2794.7000000029802,
                "duration": 312,
                "interactionId": 0,
                "processingStart": 2797.10000000149,
                "processingEnd": 2797.10000000149,
                "cancelable": true
            },
            {
                "name": "click",
                "entryType": "event",
                "startTime": 2794.7000000029802,
                "duration": 312,
                "interactionId": 5364,
                "processingStart": 2797.10000000149,
                "processingEnd": 3098.300000000745,
                "cancelable": true
            }
        ],
        "longAnimationFrameEntries": [
            {
                "name": "long-animation-frame",
                "entryType": "long-animation-frame",
                "startTime": 2796.5,
                "duration": 302,
                "renderStart": 3098.800000000745,
                "styleAndLayoutStart": 3098.900000002235,
                "firstUIEventTimestamp": 2794.7000000029802,
                "blockingDuration": 252,
                "scripts": [
                    {
                        "name": "script",
                        "entryType": "script",
                        "startTime": 2797.900000002235,
                        "duration": 300,
                        "invoker": "BUTTON.onclick",
                        "invokerType": "event-listener",
                        "windowAttribution": "self",
                        "executionStart": 2797.900000002235,
                        "forcedStyleAndLayoutDuration": 0,
                        "pauseDuration": 0,
                        "sourceURL": "http://localhost:8888/",
                        "sourceFunctionName": "onclick",
                        "sourceCharPosition": 0
                    }
                ]
            }
        ],
        "inputDelay": 2.199999999254942,
        "processingDuration": 301.3999999985099,
        "presentationDelay": 8.400000002235174,
        "loadState": "complete"
    }
}

Do we capture these and just provide a UI to view them? Do we list the counts in the admin bar and show them when clicked? Do we highlight the elements on the page which are involved in slow interactions (if they are even present)? Or do we expose a dashboard for this information as explored in #1324?

Related: #1730

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Not Started/Backlog 📆

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions