npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ui-perf/metrics

v2.1.1

Published

A developer-centric frontend performance library

Readme

Metrics

A frontend performance library for composing metrics from real user experiences.

Background

In every heavily trafficked frontend application exists a means for monitoring user experience and customer success. This library is designed to allow developers to compose metrics based on the behaviors their of end users and the performance they experience.

Getting Started

npm i -S @ui-perf/metrics
# or
yarn add @ui-perf/metrics
# or
pnpm add @ui-perf/metrics
  1. Metrics
  2. Interaction Metrics
  3. Experience Metrics
  4. Plugins
  5. CLS Plugin
  6. Critical Resource Plugin
  7. Performance Measure Plugin
  8. Building Your Own Plugins
  9. Metric Factory
  10. Demo Application

Metrics

This library's Metric API can time any experience pertinent to your end users. This can include render performance, resolution of API calls, or the duration required to complete a certain user action.

In it's most basic form, a metric an be instrumented as follows:

import { Metric } from "@ui-perf/metrics";

const MyMetric = new Metric("Initial Render");

async function AppStartup() {
  // Start the metric at the beginning of a critical user flow
  MyMetric.start();
  const response = await fetch("/ui-data");
  const data = await response.json();
  await renderDataPopulatedUI(data);
  // Stop the metric once the user action is complete
  MyMetric.stop();
}

// Optionally, listen to and record events firing:
MyMetric.on("start" | "stop" | "reset", metric => {
  // Listen for events fired!
});

Interaction Metrics

InteractionMetrics add reliability indicators to typical performance based Metrics. When using InteractionMetrics, you have the option to fail or succeed the metric based on the outcome of the interaction.

import { InteractionMetric } from "@ui-perf/metrics";

const UserSignUpMetric = new InteractionMetric("Sign Up");

async function signUp(username: string, password: string) {
  // Start the metric when the user submits the signin form or begins
  // typing into one of the fields
  UserSignUpMetric.start();
  try {
    const response = await fetch({
      method: "POST",
      url: "/sign-up",
      data: JSON.stringify({ username, password }),
    });
    const userData = await response.json();
    await redirectUserToTheHomePage(userData);
    // Succeed the metric and append non-sensitive data to the metric
    UserSignUpMetric.succeed(userData);
  } catch (error: unknown) {
    // Fail the metric and append the error to the metric
    UserSignUpMetric.fail({ error });
  }
}

// Optionally, listen to and record events firing:
UserSignUpMetric.on("success" | "failure", metric => {
  // Listen for events fired!
});

Experience Metrics

ExperienceMetrics are designed to allow developers to compose metrics from one or more sub-metrics. They are the "bigger picture" that surrounds multiple concurrent metrics running in unsion - such as multiple elements in a single page app resolving asynchronously.

The ExperienceMetric derives it's duration using the earliest start-time and the latest stop-time across all of its child-metrics.

import { Metric, ExperienceMetric } from "@ui-perf/metrics";

// Metrics for HomeScreen components
export const HeaderMetric = new Metric("Header Render Performance");
export const FooterMetric = new Metric("Footer Render Performance");
export const DashboardMetric = new Metric("Dashboard Render Performance");

// Wrap each metric in the the ExperienceMetric
export const HomeScreenMetric = new ExperienceMetric({
  name: "Home Screen Performance",
  metrics: [HeaderMetric, FooterMetric, DashboardMetric],
});

// Post the metric to your analytics service on "stop"
HomeScreenMetric.on("stop", metric => {
  void fetch("/analytics", {
    method: "POST",
    body: JSON.stringify(metric),
  });
});

In the example above, the HomeScreenMetric will have a startTime equal to the earliest start() and a stopTime equal to the latest stop() out of each of the sub-metrics.

ExperienceMetrics can accept any combination of Metrics, InteractionMetrics, and even other ExperienceMetrics. If you'd like to have a metric recording not only render performance, but the success-rate of a certain interaction, compose your ExperienceMetric using a combination of Metrics and InteractionMetrics

Plugins

Plugins are a developer API designed to enhance your metrics with any extra data or functionality your wish to add. This library comes out of the box with a few Plugins designed to assist with:

  1. Sending your metrics to the backend service of your choosing (ReporterPlugin)
  2. Tracking your metrics in relation to the most recent browser navigation (PageLoadPlugin)
  3. Tracking cumulative layout shift for metrics associated with UI features (CLSPlugin)
  4. Tracking the weight and cache-rates of critical resources required to deliver a feature or metric (CriticalResourcePlugin)
  5. Setup and testing (LoggerPlugin)

Let's dive into each plugin, then build one of our own!

Reporter Plugin

In a prior example, we subscribed to our Metric's stop event in order to send our metrics to a backend server. Using the ReporterPlugin, we can actually handle all of our metric reporting without writing individual subscriptions on each metric:

import { ReporterPlugin, ProcessingQueue } from "@ui-perf/metrics";

// This queue will batch requests to the destination specified
const Queue = new ProcessingQueue(
  "https://my-analytics-service.com",
  metrics => {
    /* 
    Format outgoing metrics in any way you wish
    and append any extra data to your request. The
    returned value will be passed directly to HTTP
    calls as the body parameter
  */
    return JSON.stringify(metrics);
  },
);

Now, let's pass our ProcessingQueue to our Metrics using the ReporterPlugin!

import {
  Metric,
  InteractionMetric,
  ExperienceMetric,
  ReporterPlugin,
} from "@ui-perf/metrics";
import { Queue } from "./MyQueue";

const MyMetric = new Metric("My Metric", {
  reporter: new ReporterPlugin(Queue),
});

const MyInteraction = new MyInteraction("My Interaction", {
  reporter: new ReporterPlugin(Queue),
});

const MyExperience = new ExperienceMetric({
  name: "My Experience",
  metrics: [MyMetric, MyInteraction],
  plugins: { reporter: new ReporterPlugin(Queue) },
});

Each of the metrics above will now automatically push their results to the Queue when their stop events are called. The Queue will then make batched post requests to the specified endpoint containing each metric's results.

By default the ProcessingQueue will attempt to Beacon the data and fallback and standard HTTP Requests.

The ReporterPlugin will also reliably push all metrics over the network if a browser session is terminated or moved into the background.

Page Load Plugin

The PageLoadPlugin allows for measuring Metric durations using the latest browser navigation. This better occumodates for metrics measuring first paint, TTI, or render times typically relative to browser navigations.

import { Metric, PageLoadPlugin } from "@ui-perf/metrics";

const ProfilePageMetric = new Metric("Profile Page", {
  pageLoad: new PageLoadPlugin(),
});

When calling ProfilePageMetric.start(), the Metric's startTime is set to the time of the last navigation. The duration of the Metric is equal to the time between the last navigation and when ProfilePageMetric.stop() is called.

CLS Plugin

Cumulative Layout Shift is a visual stability metric designed to measure the propensity for elements on the page to suddenly change positions. CLS occurs most commonly between a page's first-paint and subsequent paints where data begins populating the page. A common strategy for minimizing CLS is to render data-populated pages on the server - however, some UI features require the client to fully function.

This plugin allows for tracking the layout position of a UI element between a Metric's start() and stop() calls. On start() the plugin will capture the target element's absolute position. On stop(), the current position will be compared the position previously captured. Any differences in layout will be recorded and attached to the metric.

import { useState, useEffect, useId } from "react";
import { Metric, CLSPlugin } from "@ui-perf/metrics";

function UserAvatar({ userID }) {
  const nodeID = useId();

  const metricRef = useRef(
    new Metric("Avatar", {
      CLS: new CLSPlugin(`#${nodeID}`), // any dom selector
    }),
  );

  const [user, setUser] = useState(null);

  useEffect(() => {
    const metric = metricRef.current;
    // Start the metric on mount (this will capture the target
    // element's initial layout)
    metric.start();
    fetch(`/user/${userID}`)
      .then(response => response.json())
      .then(setUser);

    // Reset the metric on unmount or change to userID
    return () => metric.reset();
  }, [userID]);

  useEffect(() => {
    if (user) {
      // stop the metric once the user data has resolved (this
      // will capture the element's layout a second time and
      // compare it)
      metric.stop();
    }
  }, [user]);

  return (
    <div id={nodeID} className="user-avatar">
      {!user ? (
        <Loading />
      ) : (
        <>
          <img src={user.url} />
          <span>{user.name}</span>
        </>
      )}
    </div>
  );
}

The Metric found in the example above might look something like this when stop() is called:

const result = {
  name: "Avatar",
  startTime: 1000,
  stopTime: 1200,
  duration: 200,
  status: "complete",
  plugins: {
    CLS: {
      selector: "#yourNodeID",
      // The Avatar's initial boundingClientRect
      initialLayout: {
        x: 800,
        y: 200,
        top: 200,
        right: 800,
        left: 200,
        bottom: 163,
        height: 50,
        width: 50,
      },
      // A list of layout shifts that took place on Avatar between
      // metric.start() and metric.stop()
      layoutShifts: [
        {
          time: 1200,
          layoutShift: {
            width: 65,
            // a 65 pixel difference on the node's width was
            // detected at the 1200 millisecond mark
          },
        },
      ],
    },
  },
};

When using the plugin, you can inspect your target element for CLS any number of times between calls to Metric.start() and Metric.stop(). To do so invoke the CLSPlugin.inspect()

const AvatarMetric = new Metric("Avatar", {
  CLS: new CLSPlugin("#userAvatar"),
});

AvatarMetric.plugins.CLS.inspect();
// The `inspect()` method will calculate the elements current
// position and create an entry in the `layoutShifts` array
// if a shift is detected

Critical Resource Plugin

This plugin is designed to track the resources contributing to a feature's Critical Path. The plugin will calculate the total weight of JavaScript and CSS required to deliver your feature to the browser as well as the cache-rate of those resources. By default, all JavaScript and CSS resources served to the browser will be accounted for, but developers may opt in to tracking any file extensions they wish.

Let's dive into an example using our ExperienceMetric from a previous example:

import {
  Metric,
  PageLoadPlugin,
  CriticalResourcePlugin,
} from "@ui-perf/metrics";

// Home Screen sub-metrics
export const HeaderMetric = new Metric("Header Performance");
export const FooterMetric = new Metric("Footer Performance");
export const DashboardMetric = new Metric("Dashboard Performancea");

// Home Screen Experience
export const HomeScreenMetric = new ExperienceMetric({
  name: "Home Screen",
  metrics: [HeaderMetric, FooterMetric, DashboardMetric],
  plugins: {
    // Let's enable the `PageLoadPlugin` to track durations relative
    // to the browser's most recent navigation
    pageLoad: new PageLoadPlugin(),
    // Let's add our `CriticalResourcePlugin` to track Critical
    // Path and cache rate for JavaScript, CSS, and SVG's
    resources: new CriticalResourcePlugin(["js", "css", "svg"]),
  },
});

At stop(), the HomeScreenMetric's Critical Resource data will look like the following:

HomeScreenMetric.on("stop", metric => {
  /*
    HomeScreenMetric {
      "name": "Home Screen",
      "startTime": 0,
      "stopTime": 2500,
      "duration": 2500,
      "status": "complete",
      "metrics": [HeaderMetric, FooterMetric, DashboardMetric],
      "plugins": {
        // The resource-weight and cache reate of your Home Screen
        "resources": {
          "criticalSize": 200000 // (bytes),
          "cacheRate": 75 // (perceent),
          "extensions": ["js", "css", "svg"]
        }
      }
    }
  */
});

Performance Measure Plugin

This plugin allows developers to access their metrics using the native Performance API. When the PerformanceMeasurePlugin is enabled, your Metric will create a performance.measure() each time its stop() event is reached:

import { Metric, PerformanceMeasurePlugin } from "@ui-perf/metrics";

const MyMetric = new Metric("My Metric", {
  measure: new PerformanceMeasurePlugin(),
});

MyMetric.start();
MyMetric.stop();
const nativeMetric = performance.getEntriesByName("My Metric");
/*
  [{
    name: "My Metric",
    start: 123,
    end: 124,
    duration: 1
  }]
*/

Building Your Own Plugins

To build your own plugin, import the Plugin class and extend it:

import { Plugin, Metric } from "@ui-perf/metrics";

export class MyLogger extends Plugin {
  // To tap into a Metric's lifecycle, simply override one of its
  // corresponding methods:
  protected override start(metric: Metric) {
    // run some code on a metric's start
    console.log(metric.name, "Started!");
  }

  protected override stop(metric: Metric) {
    // run some code on a metric's stop
    console.log(metric.name, "Stopped!");
  }

  protected override reset(metric: Metric) {
    // run some code on a metric's reset
    console.log(metric.name, "Reset!");
  }

  // Add any public facing API you wish
  public myAttribute = true;
  public method() {
    console.log("Called my method!");
  }
}

// Add your plugin to a metric
const MyMetric = new Metric("My Metric", {
  logger: new MyLogger(),
});

// Run publicly exposed methods
MyMetric.plugins.logger.method();
// Access the current state of your plugin
MyMetric.plugins.logger.myAttribute = true;

Profiling Example

Let's build a plugin for staging/testing environments that can be helpful in catching performance regressions before they reach production.

import { Plugin, Metric } from "@ui-perf/metrics";

export class ProfilerPlugin extends Plugin {
  private static readonly enabled = import.meta.env.PROD;
  constructor(public threshold: number) {}

  protected override stop(metric) {
    if (ProfilerPlugin.enabled && metric.duration > this.threshold) {
      console.warn(
        `${metric.name} exceeded the threshold of ${this.threshold} milliseconds.`,
      );
    }
  }
}

export const MyMetric = new Metric("My Metric", {
  profiler: new ProfilerPlugin(1000),
});

Using our new plugin, MyMetric will log a warning to the console each time its duration exceeds 1000ms.

Simplifying Metric Creation

Adding the same set of plugins to every metric in your codebase can be cumbersome to maintain. To make scaffold plugins with a predefined set of plugins, this library comes with the MetricFactory.

Metric Factory

In the following example we'll assume that a resonable number of metrics are going to want to use the ReporterPlugin to post their results to an analytics server.

We'll also assume that during development we don't want our metrics posting data to servers. Here's a quick MetricFactory recipe for accomplishing that:

import {
  MetricFactory,
  LoggerPlugin,
  ReporterPlugin,
  ProcessingQueue,
} from "@ui-perf/metrics";

let Queue: ProcessingQueue | undefined;

const Plugins = {
  // Specify any Plugins you wish
  logger: LoggerPlugin,
  reporter: ReporterPlugin,
} as const;

if (import.meta.env.PROD) {
  // Remove logging in production
  delete Plugins.logger;
  // initialize the ProcessingQueue to report metrics
  // to your server
  Queue = new ProcessingQueue("/analytics");
} else {
  // Remove reporting during development and testing
  delete Plugins.reporter;
}

export const Factory = new MetricFactory(Plugins, Queue);

const MyMetric = Factory.createMetric("My Metric");

const MyInteraction = Factory.createInteraction("My Interaction");

const MyExperience = Factory.createExperience({
  name: "My Experience",
  metrics: [MyMetric, MyInteraction],
});

// In production, each metric will have the `ReporterPlugin` enabled
// During development and testing, each metric will have the
// LoggerPlugin enabled

Demo Application

To find some recipes in an example application, please reference our Demo App