Dice UI
Components

FPS

A real-time frames per second (FPS) counter component for monitoring performance.

API
import { Fps } from "@/components/ui/fps";
 
export function FpsDemo() {
  return (
    <div className="relative h-80 w-full rounded-lg border bg-muted/50">
      <Fps strategy="absolute" position="top-right" />
      <div className="flex size-full flex-col items-center justify-center gap-1">
        <div>Absolute positioning</div>
        <div className="text-muted-foreground text-sm">
          Relative to this container without a portal
        </div>
      </div>
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://diceui.com/r/fps"

Manual

Copy and paste the following code into your project.

"use client";
 
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { cn } from "@/lib/utils";
 
const fpsVariants = cva(
  "z-50 flex shrink-0 items-center gap-2 rounded-sm border bg-background/80 px-3 py-1.5 font-mono text-foreground text-sm backdrop-blur-sm",
  {
    variants: {
      strategy: {
        fixed: "fixed",
        absolute: "absolute",
      },
      position: {
        "top-left": "top-4 left-4",
        "top-right": "top-4 right-4",
        "bottom-left": "bottom-4 left-4",
        "bottom-right": "right-4 bottom-4",
      },
      status: {
        good: "text-primary",
        warning: "text-orange-500",
        error: "text-destructive",
      },
    },
    defaultVariants: {
      strategy: "fixed",
      position: "top-right",
      status: "good",
    },
  },
);
 
interface FpsProps
  extends React.ComponentProps<"div">,
    Omit<VariantProps<typeof fpsVariants>, "status"> {
  label?: string;
  updateInterval?: number;
  warningThreshold?: number;
  errorThreshold?: number;
  portalContainer?: Element | DocumentFragment | null;
  enabled?: boolean;
}
 
function Fps(props: FpsProps) {
  const {
    strategy = "fixed",
    position = "top-right",
    label,
    updateInterval = 500,
    warningThreshold = 30,
    errorThreshold = 20,
    portalContainer: portalContainerProp,
    enabled = true,
    className,
    ...fpsProps
  } = props;
 
  const [mounted, setMounted] = React.useState(false);
  const [fps, setFps] = React.useState(0);
  const frameCountRef = React.useRef(0);
  const lastTimeRef = React.useRef(performance.now());
  const animationFrameRef = React.useRef<number | null>(null);
  const updateTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(
    null,
  );
 
  React.useLayoutEffect(() => setMounted(true), []);
 
  const status = React.useMemo(() => {
    if (fps < errorThreshold) return "error";
    if (fps < warningThreshold) return "warning";
    return "good";
  }, [fps, errorThreshold, warningThreshold]);
 
  React.useEffect(() => {
    if (!enabled || typeof window === "undefined") return;
 
    function measureFps() {
      const now = performance.now();
      const delta = now - lastTimeRef.current;
      frameCountRef.current += 1;
 
      if (delta >= updateInterval) {
        const currentFps = Math.round((frameCountRef.current * 1000) / delta);
        setFps(currentFps);
        frameCountRef.current = 0;
        lastTimeRef.current = now;
      }
 
      animationFrameRef.current = requestAnimationFrame(measureFps);
    }
 
    animationFrameRef.current = requestAnimationFrame(measureFps);
 
    return () => {
      if (animationFrameRef.current !== null) {
        cancelAnimationFrame(animationFrameRef.current);
      }
      if (updateTimeoutRef.current !== null) {
        clearTimeout(updateTimeoutRef.current);
      }
    };
  }, [enabled, updateInterval]);
 
  if (!enabled) return null;
 
  const portalContainer =
    strategy === "absolute"
      ? null
      : (portalContainerProp ?? (mounted ? globalThis.document?.body : null));
 
  const Comp = (
    <div
      aria-hidden="true"
      data-slot="fps"
      {...fpsProps}
      className={cn(fpsVariants({ strategy, position, status }), className)}
    >
      {label && (
        <span data-slot="fps-label" className="text-muted-foreground">
          {label}:
        </span>
      )}
      <span data-slot="fps-value">{fps}</span>
    </div>
  );
 
  return portalContainer ? ReactDOM.createPortal(Comp, portalContainer) : Comp;
}
 
export { Fps };

Examples

Positioning Strategy

Control whether the FPS counter uses fixed or absolute positioning.

import { Fps } from "@/components/ui/fps";
 
export function FpsStrategyDemo() {
  return (
    <div className="flex w-full flex-col gap-4">
      <div className="relative h-48 w-full rounded-lg border bg-muted/50 p-4">
        <Fps strategy="absolute" position="top-right" label="Absolute" />
        <div className="flex size-full flex-col items-center justify-center gap-1">
          <div>Absolute positioning</div>
          <div className="text-muted-foreground text-sm">
            Relative to this container without a portal
          </div>
        </div>
      </div>
      <div className="relative h-48 w-full rounded-lg border bg-muted/50 p-4">
        <Fps strategy="fixed" position="bottom-right" label="Fixed" />
        <div className="flex size-full flex-col items-center justify-center gap-1">
          <div>Fixed positioning</div>
          <div className="text-muted-foreground text-sm">
            Relative to viewport with a portal
          </div>
        </div>
      </div>
    </div>
  );
}

Custom Position

Choose from four corner positions for the FPS counter.

import { Fps } from "@/components/fps"

export default function App() {
  return (
    <div>
      <Fps position="bottom-left" />
      {/* Your app content */}
    </div>
  )
}

Custom Thresholds

Configure warning and error thresholds for color-coded performance indicators.

import { Fps } from "@/components/fps"

export default function App() {
  return (
    <div>
      <Fps 
        warningThreshold={45}
        errorThreshold={30}
      />
      {/* Your app content */}
    </div>
  )
}

Conditional Rendering

Enable the FPS counter only in development environments.

import { Fps } from "@/components/fps"

export default function App() {
  const isDevelopment = process.env.NODE_ENV === "development"
  
  return (
    <div>
      <Fps enabled={isDevelopment} />
      {/* Your app content */}
    </div>
  )
}

API Reference

Fps

A component that displays a real-time FPS counter overlay.

Prop

Type

Features

  • Real-time monitoring: Uses requestAnimationFrame for accurate FPS measurement
  • Color-coded display: Automatically changes color based on performance thresholds
    • Green: Good performance (above warning threshold)
    • Yellow: Warning (below warning threshold)
    • Red: Poor performance (below error threshold)
  • Flexible positioning: Choose between fixed and absolute positioning strategies
  • Customizable position: Choose from four corner positions
  • Configurable update interval: Control how often the FPS value updates
  • Performance optimized: Minimal overhead with efficient frame counting

Positioning Strategies

Fixed Positioning

When using strategy="fixed", the FPS counter is positioned relative to the viewport and rendered via a portal into the document body. This is useful when you want the counter to remain visible while scrolling.

<Fps strategy="fixed" position="top-right" />

Absolute Positioning

When using strategy="absolute", the FPS counter is rendered directly in place (without a portal) and positioned relative to its nearest positioned ancestor. This is useful when you want the counter to be contained within a specific element with a relative wrapper.

<div className="relative">
  <Fps strategy="absolute" position="bottom-left" />
</div>

Note: Unlike fixed positioning, absolute positioning does not use a portal, so the component will respect relative positioning contexts and be contained within its parent element.

Performance Considerations

The FPS counter uses requestAnimationFrame to measure frame rate, which has minimal performance impact. The component:

  • Only updates the display at the specified interval (default: 500ms)
  • Uses refs to avoid unnecessary re-renders
  • Automatically cleans up animation frames on unmount

For production builds, consider disabling the FPS counter:

<Fps enabled={process.env.NODE_ENV === "development"} />