Adaptive performance for React. Automatically adjusts animations, effects, and rendering complexity based on what the device can actually handle.
Live Demo — Try it out and see how your device performs!
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.
npm install react-adaptive-perfimport { 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} />;
}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.
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>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,
}}
>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();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: [],
});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>Switches between animated and static images.
<AdaptiveImage
animated="/hero.gif"
static="/hero.jpg"
animationTier="medium" // Show animated version on medium+ devices
alt="Hero"
/>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>;
}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.
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" />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 }}>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' }
// }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,
// }Fully typed. Key types:
import type {
PerformanceTier,
HardwareProfile,
BenchmarkResult,
DetailedBenchmarkResult,
AdaptiveFramerMotionProps,
AdaptiveLottieProps,
} from 'react-adaptive-perf';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:
- Initial render - Starts with
tier: 'minimal'(no animations) - Page loads - Waits for
window.onloadto complete - Benchmark runs - Tests device capability when idle
- 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' }}>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 />;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
matchMediasupport
MIT © Roland Farkas