Skip to content

Adaptive performance for React. Automatically adjusts animations, effects, and rendering complexity based on what the device can actually handle.

License

Notifications You must be signed in to change notification settings

rolandfarkasCOM/react-adaptive-perf

Repository files navigation

react-adaptive-perf

Adaptive performance for React. Automatically adjusts animations, effects, and rendering complexity based on what the device can actually handle.

npm version CI license

Live Demo — Try it out and see how your device performs!

Why?

Your MacBook Pro and a budget Android phone get the same 60-particle confetti animation. One runs it smoothly. The other drops to 15fps and drains the battery.

This library detects device capability at runtime and gives you a simple API to adapt accordingly.

Install

npm install react-adaptive-perf

Quick Start

import { PerformanceProvider, useAdaptiveValue } from 'react-adaptive-perf';

function App() {
  return (
    <PerformanceProvider>
      <ParticleEffect />
    </PerformanceProvider>
  );
}

function ParticleEffect() {
  const particleCount = useAdaptiveValue({
    high: 500,
    medium: 200,
    low: 50,
    minimal: 0,
  });

  return <Particles count={particleCount} />;
}

How It Works

On mount, the library runs quick benchmarks (CSS transforms, Canvas 2D, WebGL) to measure actual rendering performance. Combined with hardware detection, it assigns one of four tiers:

Tier Typical Device Recommendation
high Modern desktop, flagship phones Full animations, all effects
medium Mid-range devices, older flagships Reduced particle counts, simpler transitions
low Budget phones, old hardware Minimal animations, static alternatives
minimal Very old devices, reduced-motion preference No animations

The minimal tier is also applied when the user has prefers-reduced-motion enabled.

API

<PerformanceProvider>

Wrap your app to enable detection.

<PerformanceProvider
  config={{
    progressiveEnhancement: true, // Start minimal, upgrade if capable
    detailedBenchmark: true,      // Run CSS + Canvas + WebGL benchmarks
    benchmarkDuration: 200,       // Duration per benchmark (ms)
    persistOverride: true,        // Remember user's manual tier selection
    onTierDetected: (tier, hardware, benchmark) => {
      console.log('Detected:', tier);
    },
  }}
>
  <App />
</PerformanceProvider>

Benchmark Control

Fine-tune benchmark behavior for better INP (Interaction to Next Paint):

<PerformanceProvider
  config={{
    // Yield to main thread every 50ms during benchmarks (default: 50)
    // Allows user interactions while benchmark runs
    // Set to 0 to disable yielding (continuous benchmark)
    benchmarkYieldInterval: 50,

    // Throttle progress callback updates (default: 100ms)
    // Reduces re-renders during benchmark
    // Set to 0 for real-time updates
    progressThrottle: 100,

    // Delay before starting benchmark (default: 0)
    // Useful to let the page settle before measuring
    benchmarkDelay: 500,
  }}
>

usePerformance()

Access the current performance context.

const {
  tier,           // 'high' | 'medium' | 'low' | 'minimal'
  isDetecting,    // true while benchmarks are running
  hardware,       // { cpuCores, deviceMemory, gpu, isMobile, ... }
  benchmark,      // { css, canvas, webgl, composite }
  shouldAnimate,  // false if tier is 'minimal'
  setTier,        // Manually override tier
  clearOverride,  // Reset to auto-detected tier
} = usePerformance();

useAdaptiveValue(values)

Return different values based on the current tier.

const animationDuration = useAdaptiveValue({
  high: 600,
  medium: 300,
  low: 150,
  minimal: 0,
});

const quality = useAdaptiveValue({
  high: 'ultra',
  medium: 'high',
  low: 'medium',
  minimal: 'low',
});

// Values cascade: if 'low' isn't defined, it uses 'medium', then 'high'
const effects = useAdaptiveValue({
  high: ['blur', 'glow', 'shadow'],
  low: ['shadow'],
  minimal: [],
});

<AdaptiveMotion>

CSS transition wrapper that adapts to performance tier.

<AdaptiveMotion
  animate={isHovered}
  initial={{ scale: 1, opacity: 0.8 }}
  target={{ scale: 1.1, opacity: 1 }}
  className="card"
>
  Hover me
</AdaptiveMotion>

<AdaptiveImage>

Switches between animated and static images.

<AdaptiveImage
  animated="/hero.gif"
  static="/hero.jpg"
  animationTier="medium"  // Show animated version on medium+ devices
  alt="Hero"
/>

Integrations

Framer Motion

Wrap Framer Motion components to automatically reduce complexity on slower devices.

import { motion } from 'framer-motion';
import { AdaptiveFramerMotion } from 'react-adaptive-perf';

<AdaptiveFramerMotion
  as={motion.div}
  animate={{ scale: 1.1, rotate: 10 }}
  transition={{ type: 'spring' }}
  disableOn={['minimal']}
>
  Animated content
</AdaptiveFramerMotion>

Or use the hook for more control:

import { useAdaptiveFramerProps } from 'react-adaptive-perf';

function Card() {
  const props = useAdaptiveFramerProps({
    animate: { scale: 1.1, rotate: 10 },
    transition: { type: 'spring', stiffness: 300 },
  });

  return <motion.div {...props}>Card</motion.div>;
}

Lottie

Automatically falls back to a static image on low-performance devices.

import { AdaptiveLottie } from 'react-adaptive-perf';

<AdaptiveLottie
  animationData={heroAnimation}
  fallbackSrc="/hero-static.webp"
  animationTier="medium"
  loop
  autoplay
/>

Requires lottie-web as a peer dependency.

Debug Panel

Add a development overlay to visualise performance metrics and test different tiers.

import { DebugPanel } from 'react-adaptive-perf';

<DebugPanel
  position="bottom-right"
  devOnly          // Auto-hide in production
  showLiveFps      // Real-time FPS counter
/>

The panel shows:

  • Current tier with live FPS
  • Individual benchmark scores (CSS, Canvas, WebGL)
  • Hardware info (CPU cores, RAM, GPU)
  • Manual tier override controls

For a minimal indicator:

import { FPSBadge } from 'react-adaptive-perf';

<FPSBadge position="top-right" />

User Preferences

By default, manual tier overrides persist to localStorage. Users who select "High" quality keep that preference across sessions.

const { setTier, clearOverride, isOverridden } = usePerformance();

// User selects a tier
setTier('high');

// Reset to auto-detected
clearOverride();

Disable persistence:

<PerformanceProvider config={{ persistOverride: false }}>

Benchmarks

The detailed benchmark runs three tests:

Test Weight What It Measures
CSS 40% DOM transforms, filters, opacity transitions
Canvas 30% 2D particle rendering with gradients
WebGL 30% GPU shader performance with 1000 points

Results are combined into a composite FPS score that determines the tier.

const { benchmark } = usePerformance();

// {
//   css: { fps: 58, jankScore: 0.02 },
//   canvas: { fps: 52, jankScore: 0.05 },
//   webgl: { fps: 45, jankScore: 0.08 },
//   composite: { fps: 52, tier: 'medium' }
// }

Hardware Detection

In addition to benchmarks, the library reads hardware signals:

const { hardware } = usePerformance();

// {
//   cpuCores: 8,
//   deviceMemory: 16,
//   isMobile: false,
//   gpu: 'ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)',
//   prefersReducedMotion: false,
//   webglSupport: 'webgl2',
//   maxTextureSize: 16384,
//   screenRefreshRate: 120,
//   devicePixelRatio: 2,
// }

TypeScript

Fully typed. Key types:

import type {
  PerformanceTier,
  HardwareProfile,
  BenchmarkResult,
  DetailedBenchmarkResult,
  AdaptiveFramerMotionProps,
  AdaptiveLottieProps,
} from 'react-adaptive-perf';

Progressive Enhancement

The recommended approach for best Core Web Vitals. Start with a fast, static experience, then upgrade to animations if the device can handle it.

<PerformanceProvider config={{ progressiveEnhancement: true }}>
  <App />
</PerformanceProvider>

How it works:

  1. Initial render - Starts with tier: 'minimal' (no animations)
  2. Page loads - Waits for window.onload to complete
  3. Benchmark runs - Tests device capability when idle
  4. Upgrade only - If device scores well, tier upgrades (never downgrades)

Benefits:

  • LCP - No animations blocking initial paint
  • INP - Benchmark runs when page is idle
  • CLS - No downgrades = no layout shifts from removing animations

You can combine with initialTier if you want to start at a different baseline:

<PerformanceProvider config={{ progressiveEnhancement: true, initialTier: 'low' }}>

SSR & Hydration

The library is SSR-safe. During server rendering and initial hydration, it uses initialTier (default: 'high', or 'minimal' with progressiveEnhancement). Detection runs after hydration completes.

To prevent hydration mismatches when rendering tier-specific content, use <WhenDetected>:

import { WhenDetected, usePerformance } from 'react-adaptive-perf';

function Hero() {
  const { tier } = usePerformance();

  return (
    <WhenDetected fallback={<HeroSkeleton />}>
      {tier === 'high' ? <HeavyAnimation /> : <LightAnimation />}
    </WhenDetected>
  );
}

Or check isDetecting manually:

const { tier, isDetecting } = usePerformance();

if (isDetecting) return <Skeleton />;
return tier === 'high' ? <Heavy /> : <Light />;

Browser Support

Works in all modern browsers. Gracefully degrades when APIs aren't available:

  • WebGL benchmark skipped if WebGL unsupported
  • Hardware memory detection falls back to estimates
  • Reduced motion detection requires matchMedia support

License

MIT © Roland Farkas

About

Adaptive performance for React. Automatically adjusts animations, effects, and rendering complexity based on what the device can actually handle.

Resources

License

Stars

Watchers

Forks