Skip to main content

Web Vitals

@xaiku/browser automatically collects Core Web Vitals and other performance metrics using the browser's PerformanceObserver API. No additional code is required beyond initializing the SDK.

Collected metrics

MetricFull nameWhat it measuresGoodNeeds improvementPoor
CLSCumulative Layout ShiftVisual stability -- unexpected layout shifts during the page lifecycle≤ 0.10.1 -- 0.25> 0.25
LCPLargest Contentful PaintLoading performance -- time to render the largest visible element≤ 2500ms2500 -- 4000ms> 4000ms
FCPFirst Contentful PaintLoading performance -- time to render the first piece of DOM content≤ 1800ms1800 -- 3000ms> 3000ms
INPInteraction to Next PaintResponsiveness -- delay between user input and the next paintMeasured via first-input delay----
TTFBTime to First ByteServer responsiveness -- time from the request to the first byte of the response≤ 800ms800 -- 1800ms> 1800ms

Each metric is automatically rated as good, needs-improvement, or poor based on the thresholds above.

How it works

When you initialize @xaiku/browser, the SDK:

  1. Creates a set of PerformanceObserver instances via makePerformanceObserver().
  2. Subscribes to the relevant entry types: layout-shift, largest-contentful-paint, paint, first-input, and navigation.
  3. Processes entries as they arrive and computes metric values.
  4. Reports each metric through the SDK event system with a rating.
import xaiku from '@xaiku/browser'

const sdk = xaiku({ pkey: 'pk_your_public_key' })

That is all you need. Web vitals are collected and reported automatically.

Metric details

CLS (Cumulative Layout Shift)

Observes layout-shift entries and accumulates shift values, excluding entries caused by recent user input (hadRecentInput). Layout shifts are grouped into sessions: shifts within 1 second of each other and within 5 seconds of the first shift in the session are combined. The metric re-initializes on back/forward cache restoration (BFCacheRestore).

LCP (Largest Contentful Paint)

Observes largest-contentful-paint entries and takes the last entry's startTime, adjusted for the navigation's activationStart. LCP observation stops after the first user interaction (keydown or click) or when the page becomes hidden, since LCP should only measure the loading phase. Re-reports on BFCacheRestore.

FCP (First Contentful Paint)

Observes paint entries and captures the first-contentful-paint entry. The value is the entry's startTime minus the navigation's activationStart. Only reported if the paint occurred before the page was first hidden. Re-reports on BFCacheRestore.

INP (Interaction to Next Paint)

Observes first-input entries and computes the input delay as processingStart - startTime. Only reported if the input occurred before the page was first hidden.

TTFB (Time to First Byte)

Observes navigation entries and uses responseStart minus the navigation's activationStart. Validates that responseStart is a positive number not in the future. Reports 0 on BFCacheRestore since cached pages have no server round-trip.

Listening to metrics

You can subscribe to metric events using the SDK's event system:

sdk.on('cls', (metric) => {
console.log('CLS:', metric.value, metric.rating)
})

sdk.on('lcp', (metric) => {
console.log('LCP:', metric.value, 'ms', metric.rating)
})

sdk.on('fcp', (metric) => {
console.log('FCP:', metric.value, 'ms', metric.rating)
})

sdk.on('ttfb', (metric) => {
console.log('TTFB:', metric.value, 'ms', metric.rating)
})

You can also listen to lifecycle events for any metric:

sdk.on('metric:cls:init', (metric) => { /* metric initialized */ })
sdk.on('metric:cls:update', (metric) => { /* metric value changed */ })
sdk.on('metric:cls:report', (metric) => { /* metric reported */ })

Metric object shape

Each reported metric contains:

FieldTypeDescription
namestringMetric name (cls, lcp, fcp, inp, ttfb).
idstringUnique identifier for this measurement (e.g., xaiku-cls-1709312000000).
valuenumberThe metric value.
ratingstring'good', 'needs-improvement', or 'poor'.
entriesPerformanceEntry[]The raw PerformanceObserver entries used to compute the value.
typestring'timeseries'
contextstring'browser.web_performance.web_vitals'
groupstring'web_performance'

Browser compatibility

Web vitals require browser support for the PerformanceObserver API with the relevant entry types. All modern browsers (Chrome, Edge, Firefox, Safari) support the core metrics. If a particular observer is not available, that metric is silently skipped.

Server-side rendering

Web vitals are only collected in browser environments. The @xaiku/node and @xaiku/nextjs server packages do not collect web vitals. When using Next.js, web vitals are collected on the client side after hydration.

Next steps