Skip to content

Commit

Permalink
feat(uPS): add auto-recovery and manual intervention
Browse files Browse the repository at this point in the history
  • Loading branch information
vladzima committed Oct 27, 2024
1 parent e3af2b7 commit dfb5d84
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 35 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Introduction

**Detecto is a React library designed to help developers automatically detect when a user's browser is experiencing performance issues such as throttling or lag**. When performance issues are detected, the application can adapt to provide a smoother, more lightweight experience for the user.
**Detecto is a React library designed to help developers automatically *detect* when a user's browser is experiencing performance issues such as throttling or lag**. When performance issues are *detected*, the application can adapt to provide a smoother, more lightweight experience for the user.

Modern websites often feature rich animations, high-resolution images, and interactive elements that can be resource-intensive, especially on older or low-powered devices. By using Detecto, you can *detect* when the user's browsing environment is struggling and adjust your UI dynamically to keep the user experience optimal, even under less-than-ideal conditions.

Expand All @@ -12,10 +12,11 @@ Modern websites often feature rich animations, high-resolution images, and inter
- **Frame Rate Monitoring**: Track frame rate (FPS) to identify if the user's device is struggling to keep up with animations or other tasks.
- **Long Task Detection**: Use the `PerformanceObserver` API to monitor long-running tasks that could affect responsiveness.
- **Initial Sampling Period**: Average frame rates over an initial period (default is 5 seconds) to avoid false positives during the initial page load.
- **Page Visibility Handling**: Detecto pauses performance monitoring when the page is inactive to prevent misleading metrics such as `NaN` for FPS when the tab is not visible.
- **Customizable Parameters**: Easily adjust detection thresholds to suit your specific needs or let the library use its defaults.
- **Auto-Recovery and Manual Intervention**: Choose between automatic recovery with a cooldown (`lagRecoveryDelay`) or manual intervention to reset the lagging state.
- **Sane defaults and extreme flexibility**: Easily adjust detection thresholds and behavior to suit your specific needs or let the library use its defaults.
- **React Hooks**: Provides easy integration through a `usePerformance` hook to access lagging status wherever you need in your application.
- **Fallback Handling**: You can optionally define custom behavior when the environment does not support performance detection features.
- **Insightful Logging**: Detecto logs key metrics in the development environment at meaningful intervals to help you debug performance issues.

Whether you're building a highly interactive web application or an e-commerce site, Detecto ensures your users enjoy the best experience, regardless of their hardware capabilities or the conditions under which they browse.

Expand Down Expand Up @@ -67,6 +68,7 @@ With the default configuration, the Detecto library will:
- Check performance every second (`checkInterval` of 1000ms).
- Average FPS over an initial sampling period of 5 seconds (`initialSamplingDuration` of 5000ms) to prevent false positives during initial page load.
- Pause performance monitoring when the page is inactive to prevent misleading metrics.
- Automatically recover from lagging state after a cooldown (`lagRecoveryDelay` of 3000ms) or provide a manual reset option.
## Browser Requirements
This library uses `PerformanceObserver` to detect performance issues in the browser. Please note the following:
Expand All @@ -92,18 +94,23 @@ const MyComponent: React.FC = () => {
longTaskThreshold: 50, // Adjust the threshold for long tasks (in milliseconds)
checkInterval: 1000, // Adjust the interval (in milliseconds) to check for performance issues
initialSamplingDuration: 5000, // Adjust the initial sampling duration if needed
lagRecoveryDelay: 3000, // Adjust the delay (in milliseconds) to recover from lag
autoRecover: false, // Set to false for manual intervention
onFeatureNotAvailable: () => {
console.warn("Performance features are not available, running fallback behavior...");
// Here you could disable some animations, show a fallback UI, etc.
},
};
const isLagging = usePerformanceStatus(config);
const { isLagging, resetLagging } = usePerformanceStatus(config);
return (
<div>
{isLagging ? (
<div>Rendering lightweight version...</div>
<div>
Rendering lightweight version...
<button onClick={resetLagging}>Reset Lagging State</button>
</div>
) : (
<div>Rendering heavy, animated version...</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.2.2",
"version": "0.3.2",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
15 changes: 12 additions & 3 deletions src/PerformanceProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import {
ThrottleDetectionConfig,
} from './usePerformanceStatus';

const PerformanceContext = createContext<boolean>(false);
interface PerformanceContextType {
isLagging: boolean;
resetLagging: () => void;
}

const PerformanceContext = createContext<PerformanceContextType>({
isLagging: false,
resetLagging: () => {},
});

interface PerformanceProviderProps extends React.PropsWithChildren {
config?: ThrottleDetectionConfig;
Expand All @@ -14,9 +22,10 @@ export const PerformanceProvider: React.FC<PerformanceProviderProps> = ({
config,
children,
}) => {
const isLagging = usePerformanceStatus(config);
const performanceStatus = usePerformanceStatus(config);

return (
<PerformanceContext.Provider value={isLagging}>
<PerformanceContext.Provider value={performanceStatus}>
{children}
</PerformanceContext.Provider>
);
Expand Down
51 changes: 25 additions & 26 deletions src/usePerformanceStatus.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';

export interface ThrottleDetectionConfig {
fpsThreshold?: number; // e.g., below 20 FPS
longTaskThreshold?: number; // e.g., tasks longer than 50ms
checkInterval?: number; // Interval to check for lag (e.g., every 1 second)
initialSamplingDuration?: number; // Time period for extended initial sampling (in milliseconds)
lagRecoveryDelay?: number; // Cooldown for switching back from lagging state
autoRecover?: boolean; // Option to control if lagging state should auto-recover
onFeatureNotAvailable?: () => void; // Callback if a required feature is not available
}

Expand All @@ -13,14 +15,15 @@ const defaultConfig: ThrottleDetectionConfig = {
longTaskThreshold: 50,
checkInterval: 1000,
initialSamplingDuration: 5000,
lagRecoveryDelay: 3000,
autoRecover: true,
};

export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
const [isLagging, setIsLagging] = useState(false);
const effectiveConfig = { ...defaultConfig, ...config };

let lastLogTime = 0;
let pageVisible = true;
let lastLaggingTime = 0;

useEffect(() => {
if (typeof window === 'undefined') {
Expand Down Expand Up @@ -55,19 +58,8 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
initialSamplingComplete = true;
}, effectiveConfig.initialSamplingDuration);

// Handle page visibility change
function handleVisibilityChange() {
pageVisible = !document.hidden;
if (!pageVisible) {
// Clear FPS samples when page becomes hidden to avoid calculating NaN
fpsSamples = [];
}
}

document.addEventListener('visibilitychange', handleVisibilityChange);

function trackFrameRate() {
if (!pageVisible) {
if (document.hidden) {
return;
}

Expand All @@ -80,26 +72,32 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
}

function checkPerformance() {
if (!pageVisible || fpsSamples.length === 0) {
// If page is not visible or no frames were sampled, skip calculation
if (fpsSamples.length === 0) {
// No frames were sampled, possibly due to inactivity.
return;
}

const averageFPS =
fpsSamples.reduce((a, b) => a + b, 0) / fpsSamples.length;

if (initialSamplingComplete) {
const now = Date.now();

if (averageFPS < effectiveConfig.fpsThreshold!) {
setIsLagging(true);
} else {
lastLaggingTime = now;

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect

Check warning on line 88 in src/usePerformanceStatus.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Assignments to the 'lastLaggingTime' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect
} else if (
effectiveConfig.autoRecover &&
isLagging &&
now - lastLaggingTime > effectiveConfig.lagRecoveryDelay!
) {
setIsLagging(false);
}
}

// Log key metrics in development environment at meaningful intervals
if (process.env.NODE_ENV === 'development') {
const currentTime = Date.now();
if (currentTime - lastLogTime > 10000) {
if (currentTime - lastLaggingTime > 10000) {
// Log every 10 seconds
if (!isNaN(averageFPS)) {
console.log(`Average FPS: ${averageFPS.toFixed(2)}`);
Expand All @@ -111,16 +109,15 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
'No frames rendered during this interval (page possibly idle).'
);
}
lastLogTime = currentTime;
lastLaggingTime = currentTime;
}
}

// Reset FPS samples for the next interval
fpsSamples = [];
}

const observer = new PerformanceObserver(list => {
if (!pageVisible) {
if (document.hidden) {
return;
}

Expand All @@ -131,7 +128,6 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
) {
setIsLagging(true);

// Log long task in development environment
if (process.env.NODE_ENV === 'development') {
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`);
}
Expand Down Expand Up @@ -159,9 +155,12 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
clearInterval(frameInterval);
clearInterval(checkInterval);
observer.disconnect();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [config]);

return isLagging;
const resetLagging = useCallback(() => {
setIsLagging(false);
}, []);

return { isLagging, resetLagging };
}

0 comments on commit dfb5d84

Please sign in to comment.