Skip to content

Commit

Permalink
feat(usePerformanceStatus): introduce idle detection, visibility api,…
Browse files Browse the repository at this point in the history
… dev env logging
  • Loading branch information
vladzima committed Oct 27, 2024
1 parent 9f3eb83 commit 6f56ea5
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 48 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ Modern websites often feature rich animations, high-resolution images, and inter

## Key Features

- **Lightweight**: Detecto is only ~850B, making it easy to integrate into your project.
- **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.
- **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.
Expand All @@ -18,10 +21,14 @@ Whether you're building a highly interactive web application or an e-commerce si

## Quickstart: Basic Usage

1. Install the library with `npm i detecto` or `yarn add detecto`.
1. Install the library:
```bash
npm i detecto
```

2. Use the `PerformanceProvider` and `usePerformance` Hook in your app.

Example:
Example (`.tsx`):

```tsx
import React from 'react';
Expand Down Expand Up @@ -58,6 +65,8 @@ With the default configuration, the Detecto library will:
- Detect low frame rates (`fpsThreshold` of 20).
- Monitor for long tasks exceeding 50ms.
- 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.
## Browser Requirements
This library uses `PerformanceObserver` to detect performance issues in the browser. Please note the following:
Expand All @@ -79,9 +88,10 @@ import { usePerformanceStatus, ThrottleDetectionConfig } from "detecto";
const MyComponent: React.FC = () => {
const config: ThrottleDetectionConfig = {
fpsThreshold: 20,
longTaskThreshold: 50,
checkInterval: 1000,
fpsThreshold: 20, // Adjust the FPS threshold to determine when lagging is detected
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
onFeatureNotAvailable: () => {
console.warn("Performance features are not available, running fallback behavior...");
// Here you could disable some animations, show a fallback UI, etc.
Expand Down
1 change: 0 additions & 1 deletion global.d.ts

This file was deleted.

9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1.0",
"version": "0.2.2",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -58,6 +58,7 @@
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.8.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/testing-library__jest-dom": "^6.0.0",
Expand Down
82 changes: 77 additions & 5 deletions src/usePerformanceStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ 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)
onFeatureNotAvailable?: () => void; // Callback if a required feature is not available
}

const defaultConfig: ThrottleDetectionConfig = {
fpsThreshold: 20,
longTaskThreshold: 50,
checkInterval: 1000,
initialSamplingDuration: 5000,
};

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

let lastLogTime = 0;
let pageVisible = true;

useEffect(() => {
if (typeof window === 'undefined') {
console.error(
Expand All @@ -38,9 +43,34 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
return;
}

if (process.env.NODE_ENV === 'development') {
console.log('Detecto has started monitoring performance.');
}

let fpsSamples: number[] = [];
let initialSamplingComplete = false;

// Set timeout for the initial sampling period
setTimeout(() => {
initialSamplingComplete = true;
}, effectiveConfig.initialSamplingDuration);

// Handle page visibility change
function handleVisibilityChange() {
pageVisible = !document.hidden;

Check warning on line 60 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 'pageVisible' 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 60 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 'pageVisible' 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 60 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 'pageVisible' 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 60 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 'pageVisible' 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
if (!pageVisible) {
// Clear FPS samples when page becomes hidden to avoid calculating NaN
fpsSamples = [];
}
}

document.addEventListener('visibilitychange', handleVisibilityChange);

function trackFrameRate() {
if (!pageVisible) {
return;
}

const start = performance.now();
requestAnimationFrame(() => {
const end = performance.now();
Expand All @@ -50,20 +80,61 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
}

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

const averageFPS =
fpsSamples.reduce((a, b) => a + b, 0) / fpsSamples.length;
if (averageFPS < effectiveConfig.fpsThreshold!) {
setIsLagging(true);
} else {
setIsLagging(false);

if (initialSamplingComplete) {
if (averageFPS < effectiveConfig.fpsThreshold!) {
setIsLagging(true);
} else {
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) {
// Log every 10 seconds
if (!isNaN(averageFPS)) {
console.log(`Average FPS: ${averageFPS.toFixed(2)}`);
if (isLagging) {
console.warn('Performance warning: FPS is below the threshold.');
}
} else {
console.log(
'No frames rendered during this interval (page possibly idle).'
);
}
lastLogTime = currentTime;

Check warning on line 114 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 'lastLogTime' 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 114 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 'lastLogTime' 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 114 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 'lastLogTime' 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 114 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 'lastLogTime' 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
}
}

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

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

list.getEntries().forEach(entry => {
if (entry.duration > effectiveConfig.longTaskThreshold!) {
if (
entry.duration > effectiveConfig.longTaskThreshold! &&
initialSamplingComplete
) {
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 All @@ -88,6 +159,7 @@ export function usePerformanceStatus(config?: ThrottleDetectionConfig) {
clearInterval(frameInterval);
clearInterval(checkInterval);
observer.disconnect();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [config]);

Expand Down
28 changes: 0 additions & 28 deletions test/HeavyComponent.test.tsx

This file was deleted.

6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"include": ["src", "types", "test", "global.d.ts"],
"include": ["src", "types"],
"compilerOptions": {
"module": "esnext",
"types": ["jest", "@testing-library/jest-dom"],
"typeRoots": ["./node_modules/@types", "./global.d.ts"],
"types": ["jest", "node"],
"typeRoots": ["./node_modules/@types"],
"lib": ["dom", "esnext"],
"importHelpers": true,
"declaration": true,
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1623,7 +1623,7 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==

"@types/node@*", "@types/node@>=10.0.0":
"@types/node@*", "@types/node@^22.8.1", "@types/node@>=10.0.0":
version "22.8.1"
resolved "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz"
integrity sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==
Expand Down

0 comments on commit 6f56ea5

Please sign in to comment.