# Collect Real Core Web Vitals From Your Visitors

> Measures Largest Contentful Paint, Cumulative Layout Shift and Interaction to Next Paint in visitors' browsers and shows the 75th percentile of the last 7 days per page type in a dashboard widget.

- Type: PHP
- Runs: Run everywhere
- Category: [Performance](https://wpide.com/library/category/performance/)
- Tags: core-web-vitals, monitoring, dashboard
- Author: WPIDE Team
- Installs: 0
- Updated: 2026-10-28
- Page: https://wpide.com/library/perf-web-vitals-collector/

## Code

```php
// Share of page views that report (0.2 = 20%).
$wpide_vitals_sample = 0.2;

add_action( 'rest_api_init', function () {
    register_rest_route( 'wpide/v1', '/vitals', array(
        'methods'             => 'POST',
        'permission_callback' => '__return_true',
        'args'                => array(
            'type' => array( 'type' => 'string', 'enum' => array( 'home', 'single', 'page', 'archive', 'product', 'other' ), 'required' => true ),
            'lcp'  => array( 'type' => 'number', 'minimum' => 0, 'maximum' => 60000 ),
            'cls'  => array( 'type' => 'number', 'minimum' => 0, 'maximum' => 10 ),
            'inp'  => array( 'type' => 'number', 'minimum' => 0, 'maximum' => 60000 ),
        ),
        'callback'            => function ( WP_REST_Request $request ) {
            $day   = gmdate( 'Ymd' );
            $store = (array) get_option( 'wpide_vitals', array() );
            $type  = $request['type'];

            foreach ( array( 'lcp', 'cls', 'inp' ) as $metric ) {
                if ( null === $request[ $metric ] ) {
                    continue;
                }
                $values = isset( $store[ $day ][ $type ][ $metric ] ) ? $store[ $day ][ $type ][ $metric ] : array();
                if ( count( $values ) < 500 ) {
                    $values[] = 'cls' === $metric ? round( (float) $request[ $metric ], 3 ) : (int) $request[ $metric ];
                }
                $store[ $day ][ $type ][ $metric ] = $values;
            }

            krsort( $store );
            update_option( 'wpide_vitals', array_slice( $store, 0, 7, true ), false );

            return new WP_REST_Response( null, 204 );
        },
    ) );
} );

add_action( 'wp_footer', function () use ( $wpide_vitals_sample ) {
    if ( is_user_logged_in() ) {
        return;
    }

    $type = is_front_page() ? 'home' : ( function_exists( 'is_product' ) && is_product() ? 'product' : ( is_single() ? 'single' : ( is_page() ? 'page' : ( is_archive() || is_home() ? 'archive' : 'other' ) ) ) );
    ?>
<script>
(function () {
    if (Math.random() > <?php echo (float) $wpide_vitals_sample; ?> || !('PerformanceObserver' in window)) { return; }
    var data = { type: <?php echo wp_json_encode( $type ); ?> }, cls = 0, inp = 0;
    function observe(type, cb) { try { new PerformanceObserver(function (l) { l.getEntries().forEach(cb); }).observe({ type: type, buffered: true }); } catch (e) {} }
    observe('largest-contentful-paint', function (e) { data.lcp = Math.round(e.startTime); });
    observe('layout-shift', function (e) { if (!e.hadRecentInput) { cls += e.value; data.cls = Math.round(cls * 1000) / 1000; } });
    observe('event', function (e) { if (e.interactionId && e.duration > inp) { inp = e.duration; data.inp = Math.round(inp); } });
    var sent = false;
    function send() {
        if (sent || document.visibilityState !== 'hidden') { return; }
        sent = true;
        navigator.sendBeacon && navigator.sendBeacon(<?php echo wp_json_encode( esc_url_raw( rest_url( 'wpide/v1/vitals' ) ) ); ?>, new Blob([JSON.stringify(data)], { type: 'application/json' }));
    }
    document.addEventListener('visibilitychange', send);
})();
</script>
    <?php
}, 99 );

add_action( 'wp_dashboard_setup', function () {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    wp_add_dashboard_widget( 'wpide_vitals', 'Core Web Vitals (last 7 days, p75)', function () {
        $merged = array();
        foreach ( (array) get_option( 'wpide_vitals', array() ) as $types ) {
            foreach ( $types as $type => $metrics ) {
                foreach ( $metrics as $metric => $values ) {
                    $merged[ $type ][ $metric ] = array_merge( isset( $merged[ $type ][ $metric ] ) ? $merged[ $type ][ $metric ] : array(), $values );
                }
            }
        }
        if ( ! $merged ) {
            echo '<p>No measurements yet. Data appears after visitors browse the site.</p>';
            return;
        }
        $p75 = function ( $values ) {
            sort( $values );
            return $values ? $values[ (int) floor( 0.75 * ( count( $values ) - 1 ) ) ] : null;
        };
        echo '<table class="widefat striped"><thead><tr><th>Page type</th><th>LCP</th><th>CLS</th><th>INP</th><th>Samples</th></tr></thead><tbody>';
        foreach ( $merged as $type => $m ) {
            $lcp = isset( $m['lcp'] ) ? $p75( $m['lcp'] ) : null;
            $cls = isset( $m['cls'] ) ? $p75( $m['cls'] ) : null;
            $inp = isset( $m['inp'] ) ? $p75( $m['inp'] ) : null;
            printf(
                '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td></tr>',
                esc_html( $type ),
                null === $lcp ? '-' : esc_html( round( $lcp / 1000, 2 ) . 's' ),
                null === $cls ? '-' : esc_html( $cls ),
                null === $inp ? '-' : esc_html( $inp . 'ms' ),
                isset( $m['lcp'] ) ? count( $m['lcp'] ) : 0
            );
        }
        echo '</tbody></table><p class="description">Good: LCP under 2.5s, CLS under 0.1, INP under 200ms.</p>';
    } );
} );
```

## Notes

Samples 20% of page views by default and stores only numbers per page type, no visitor data; values are rounded and capped to keep storage tiny.

## How to use it

Install the free WPIDE plugin (https://wordpress.org/plugins/wpide/), find "Collect Real Core Web Vitals From Your Visitors" in its snippet library and add it in one click; or create a new PHP snippet in WPIDE (or any code snippets plugin) and paste the code above. More snippets: https://wpide.com/library/
