Skip to content
/ uxm Public

A modular library for collecting front-end performance metrics.

License

Notifications You must be signed in to change notification settings

treosh/uxm

Repository files navigation

An utility library for collecting user-centric performance metrics.

Why?UsageAPICredits



Features:

Usage

npm install uxm@next

Collect user-centric metrics and send data to your API (1.5Kb):

import { collectMetrics, createApiReporter, getDeviceInfo } from 'uxm'

const report = createApiReporter('/api/collect', { initial: getDeviceInfo() })

collectMetrics(['fcp', 'lcp', 'fid', 'cls'], ({ metricType, value }) => {
  report({ [metricType]: value })
})

At the end of the session (on visibilitychange event), your API receives a POST request (using sendBeacon) with data for core UX metrics and a device information, like:

{
  "fcp": 1409,
  "fid": 64,
  "lcp": 2690,
  "cls": 0.025,
  "url": "https://example.com/",
  "memory": 8,
  "cpus": 2,
  "connection": { "effectiveType": "4g", "rtt": 150, "downlink": 4.25 }
}

Explore examples for building a robust real-user monitoring (RUM) logic. Size of each example is controlled using size-limit.

Report FCP and FID to Google Analytics (0.7 KB)

Use Google Analytics as a free RUM service, and report user-centric performance metrics. Learn more about using Google Analytics for site speed monitoring.

google-analytics-reporter.js:

import { collectFcp, collectFid } from 'uxm'

collectFcp(reportToGoogleAnalytics)
collectFid(reportToGoogleAnalytics)

function reportToGoogleAnalytics(metric) {
  ga('send', 'event', {
    eventCategory: 'Performance Metrics',
    eventAction: 'track',
    [metric.metricType]: metric.value,
  })
}
Measure React view render performance (0.65 KB)

A react-hook example that measures rendering performance and creates a custom user-timing measure.

react-use-time-hook.js:

import { time, timeEndPaint } from 'uxm'

export function App() {
  useTime('render:app')
  return 'Hello from React'
}

function useTime(label) {
  time(label) // render started
  useEffect(() => timeEndPaint(label), []) // render ended, and the browser paint has been procceed.
}
Build a custom layout-shift metric for SPA (0.8 KB)

Layout Instability is a flexible API that allows building custom metrics on top — like, measuring cumulative layout shift per view, not the whole session.

custom-layout-shift.js:

import { observeEntries } from 'uxm'
import { observeHistory } from 'uxm/experimental'

/** @type {{ url: string, cls: number }[]} */
let views = []
let cls = 0

// cummulate `layout-shift` values, with an input

observeEntries('layout-shift', (layoutShiftEntries) => {
  layoutShiftEntries.forEach((e) => {
    if (!e.hadRecentInput) cls += e.value
  })
})

// observe `history` changes,
// and reset `cls` when a route changes

observeHistory((e) => {
  views.push({ url: e.prevUrl, cls })
  cls = 0
})
Collect CrUX-like metrics (1.6Kb)

Chrome UX Report (CrUX) is a great way to see how real-world Chrome users experience the speed of your website. But for privacy reasons, CrUX aggregates data only per origin.

This script collects detailed crux-like analytics on the URL level.

crux-metrics.js:

import { getDeviceInfo, collectLoad, collectFcp, collectLcp, collectFid, collectCls, onVisibilityChange } from 'uxm'

// init `metrics` and get device information

const { connection, url } = getDeviceInfo()
const metrics = { url, effectiveConnectionType: connection.effectiveType }

// collect loading metrics

collectLoad(({ value: load, detail: { domContentLoaded, timeToFirstByte } }) => {
  metrics.timeToFirstByte = timeToFirstByte
  metrics.domContentLoaded = domContentLoaded
  metrics.load = load
})

// collect user-centric metrics

collectFcp(({ value }) => (metrics.firstContentfulPaint = value))
collectLcp(({ value }) => (metrics.largestContentfulPaint = value))
collectFid(({ value }) => (metrics.firstInputDelay = value))
collectCls(({ value }) => (metrics.cumulativeLayoutShift = value))

// all metrics are collected on "visibilitychange" event

onVisibilityChange(() => {
  console.log(metrics)
  //  {
  //    "url": "https://example.com/",
  //    "effectiveConnectionType": "4g",
  //    "timeToFirstByte": 1204,
  //    "domContentLoaded": 1698,
  //    "load": 2508
  //    "firstContentfulPaint": 1646,
  //    "largestContentfulPaint": 3420,
  //    "firstInputDelay": 12,
  //    "cumulativeLayoutShift": 0.12,
  //  }
}, 1)

API

Metrics

Metrics are the core of uxm (uxm is a 3-letter acronym that stands for User eXperience Metrics).

It focuses on metrics, that captures a user experience, instead of measuring technical details, that are easy to manipulate. This metrics are more representetive for a user, and the final purpose of a good frontend is to create a delightful user experience.

Each metric follows the structure:

  • metricType <[string]> - a metric acronym, ex: lcp, fid, or cls.
  • value <number> - a numeric value of a metric, ex: 1804 for lcp, 4 for fid, or 0.129 for cls.
  • detail <object> - an extra detail specific for an each metric, like elementSelector for lcp, event name for fid, or totalEntries for cls.

with an exception for collectLoad (it does not have a 3-letters acronym, and considered a legacy.) Use a per-metric function for more granular control of the callback behavior and saving a bundle size.

This metrics are only available in Chromium-based browsers (Chrome, Edge, Opera).

The best way to understand a metric is to read web.dev/metrics and check the source.

collectMetrics(metrics, callback)

The method is a shortcut for calling collectFcp, collectFid, collectLcp, and collectCls.

import { collectMetrics } from 'uxm'

const report = createApiReporter('/api/collect')

// pass a metric 3-letter acronym
collectMetrics(['fcp', 'fid'], (metric) => {
  report({ [metric.metricType]: metric.value })
})

// or a metric options using an object and `type`
collectMetrics([{ type: 'lcp', maxTimeout: 1000 }], (metric) => {
  report({ lcp: metric.value })
})

collectFcp(callback)

  • callback <function> a callback with FcpMetric:
    • metricType <"fcp">
    • value <number> a time when the user can see anything on the screen – a fast FCP helps reassure the user that something is happening.

Collect First Contentful Paint (FCP) using paint entries.

collectFid(callback)

import { collectFid } from 'uxm'

collectFid((metric) => {
  console.log(metric)
  // { metricType: "fid", value: 1, detail: { duration: 8, startTime: 2568.1, processingStart: 2568.99, processingEnd: 2569.02, name: "mousedown" }
})

collectLcp(callback, [options])

  • callback <function> a callback with LcpMetric:
    • metricType <"lcp">
    • value <number> a time when the page's main content has likely loaded – a fast LCP helps reassure the user that the page is useful.
    • detail <object>
      • elementSelector <[string]> CSS selector of an element, that is triggered the most significant paint
      • size <number> size (height x width) of the largest element
  • options <object> (Optional)
    • maxTimeout <number> The longest delay between largest-contentful-paint entries to consider the LCP. Defaults to 10000 ms.

Collect Largest Contentful Paint (LCP) using largest-contentful-paint entries. A callback triggers when a user interacts with a page, or after maxTimeout between entries, or on "visibilitychange" event.

import { collectLcp } from 'uxm'

collectLcp((metric) => {
  console.log(metric) // { metricType: "lcp", value: 2450, detail: { size: 8620, elementSelector: "body > h1" } }
})

collectCls(callback, [options])

import { collectCls } from 'uxm'

collectCls(
  (metric) => {
    console.log(metric) // { metricType: "cls", value: 0.0893, detail: { totalEntries: 2, sessionDuration: 2417 } }
  },
  { maxTimeout: 1000 }
)

collectLoad(callback)

import { collectLoad } from 'uxm'

collectLoad(({ value: load, detail: { domContentLoaded, timeToFirstByte } }) => {
  console.log({ timeToFirstByte, domContentLoaded, load })
})

Performance Observer

observeEntries(options, callback)

getEntriesByType(entryType)

Reporter

createApiReporter(url, [options])

User Timing

mark(markName, [markOptions])

measure(markName, [startOrMeasureOptions], [endMarkName])

time(label, [startLabel])

timeEnd(label, [startLabel])

timeEndPaint(label, [startLabel])

now()

Device Info

getDeviceInfo()

Experimental (alpha)

collectCid(callback)

observeHistory(callback)

recordTrace(callback, [options])

calcSpeedScore(values, [ranks])


Credits

Made with ❤️ by Treo.