npm.io
1.2.0 • Published 11h ago

lite-fps-meter

Licence
MIT
Version
1.2.0
Deps
0
Size
40 kB
Vulns
0
Weekly
0

lite-fps-meter

npm version sponsor npm bundle size npm downloads npm total downloads TypeScript Zero Dependencies License: MIT

A lightweight, zero-dependency FPS monitor that renders a real-time graph overlay on a canvas element.

Drop it into any web project during development to spot jank, profile animations, or verify you're hitting your target frame rate. Auto-detects the display refresh rate (60Hz, 120Hz, 144Hz, etc.) and adapts thresholds automatically.

Live Demo (SmartObserver)

https://codepen.io/Zahari-Shinikchiev/debug/LERWgyQ

Features

  • Zero dependencies — single ES module, no build step required
  • Auto-detects refresh rate — adapts target from 30Hz to 240Hz displays
  • Canvas-rendered graph — scrolling history bar chart with color-coded thresholds
  • Compact mode — text-only readout when graph: false (15px tall)
  • EMA smoothing — configurable exponential moving average for stable readouts
  • Themeable — override any color (good/ok/bad/bg/mid/detecting)
  • Configurable — position, dimensions, smoothing factor, mount target
  • Clean teardowndestroy() cancels all rAF frames and removes DOM elements

Installation

npm install lite-fps-meter

Or drop the file directly into your project — it's a single ES module.

Quick Start

import { FPSMeter } from 'lite-fps-meter';

// Create and start (auto-attaches to document.body)
const meter = new FPSMeter();

// Later: clean up
meter.destroy();

A fixed-position overlay appears in the top-left corner showing current FPS, min/max range, and a scrolling bar graph.

Options

const meter = new FPSMeter({
    width: 120,              // Canvas width (px)
    height: 50,              // Canvas height (px) — auto-shrinks to 15 when graph: false
    graph: true,             // Show scrolling bar graph (false = compact text-only)
    graphHeight: 30,         // Graph area height (px)
    textUpdateInterval: 100, // Min ms between text refreshes
    smoothing: 0.1,          // EMA factor (0–1). Lower = smoother, higher = responsive
    position: 'top-left',    // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
    target: null,            // Mount target element (default: document.body)
    targetFps: null,         // Pin the budget (skips auto-detect) — e.g. 60, 144
    theme: {                 // Color overrides (merged with defaults)
        good: '#0f0',        // verdict: steady
        ok: '#ff0',          // verdict: spiking
        bad: '#f00',         // verdict: throttled
        bg: '#111',          // Background
        mid: '#800',         // 50% target line
        detecting: '#aaa',   // Text during refresh rate detection
    },
});
Compact Mode

For a minimal footprint, disable the graph. The meter shrinks to a 15px-tall text-only readout:

const meter = new FPSMeter({ graph: false, position: 'bottom-right' });

API

Method Description
new FPSMeter(options?) Create meter and start measuring immediately
.pause() Pause the measurement loop
.resume() Resume after pause (resets timestamp to avoid delta spike)
.reset() Clear min/max counters, history graph, dropped count, and refresh text
.getStats() Return a plain stats object (see below). One fresh object per call, zero allocation per frame.
.destroy() Stop everything, remove DOM elements. Idempotent.
getStats()

Returns a snapshot for programmatic reads — the escape hatch for anyone who wants the numbers without reading the overlay:

const s = meter.getStats();
// { fps, min, max, minMs, maxMs, p50, p99, budgetMs, targetFPS,
//   dropped, jankRatio, spikeRatio, frameClass, samples, detecting }
  • dropped — cumulative missed vsyncs since reset, defined as sum of max(0, round(delta / budgetMs) - 1). A perfectly paced frame counts 0; a 200 ms frame at 60 Hz counts 11. This exact definition is what makes the number comparable across runs.
  • p50 / p99 — median and p99 raw frame time in ms, computed on the text tick (never per frame). Window-relative: over at most width retained samples, not the whole session. A 120-sample p99 is a weak statistic — treat it as "the tail of the recent window", not a session-wide claim.
  • frameClass'steady' / 'spiking' / 'throttled'. See below.

Properties

Property Type Description
.fps number Current smoothed FPS (EMA)
.min number Minimum FPS since last reset
.max number Maximum FPS since last reset
.dropped number Cumulative dropped (missed-vsync) frames since last reset
.targetFPS number Detected display refresh rate (or the targetFps override)
.showGraph boolean Whether the bar graph is rendered
.smoothing number EMA smoothing factor
.theme object Active color theme

The verdict (color)

The overlay color is driven by a budget-relative verdict so color and verdict can never contradict. It deliberately mirrors @zakkster/lite-profiler's FrameClass names and thresholds so the drop-in overlay and the rigorous profiler tell the same story.

fps-meter measures frame interval (a healthy 60 Hz frame is ~16.67 ms), not work time, so the jank/spike edges are relative to the budget, not the absolute 16/33 ms lite-profiler uses — otherwise every healthy 60 Hz frame would be flagged as jank. Over the retained window:

  • jankRatio = fraction of samples with ms >= 1.5 × budgetMs
  • spikeRatio = fraction with ms >= 2 × budgetMs (reported, not used by the verdict)
Color frameClass Condition (jankRatio only, exactly lite-profiler's classify())
Green steady jankRatio < 0.05
Yellow spiking 0.05 <= jankRatio < 0.25
Red throttled jankRatio >= 0.25

The verdict is computed over the current window and recovers when jank stops.

How It Works

Refresh rate detection: On creation, the meter counts requestAnimationFrame callbacks over a 250ms window and derives the average frame rate. Background-throttled frames (>100ms) are discarded. The result is clamped to 30–240Hz. Detection survives pause — it resumes timing when the meter restarts.

FPS calculation: Each frame computes an instantaneous FPS from the delta, then applies an exponential moving average: fps = fps + (instant - fps) * smoothing. Zero and negative deltas (tab resume, first frame) are skipped to prevent Infinity from polluting the average.

Graph rendering: A Uint8Array ring buffer stores the last N frame heights. On each draw, the buffer is read from the current index (oldest) to produce a left-to-right scrolling chart. Two reference lines mark 100% and 50% of the target FPS. In compact mode, the buffer is never written and the bar loop is skipped entirely.

TypeScript

Full type definitions included:

import { FPSMeter, type FPSMeterOptions, type FPSMeterTheme } from 'lite-fps-meter';

const meter = new FPSMeter({
    position: 'bottom-right',
    theme: { good: '#00ff88' },
});

License

MIT

Keywords