Fix performance issue: debounce resize event listeners
- Add useDebounce hook for value debouncing - Add useWindowSize hook for debounced window dimensions - Add useElementSize hook for debounced element size tracking with ResizeObserver - Replace undebounced resize listeners in App.tsx, simulation-chart.tsx, and settings.tsx - Prevents excessive re-renders during window resize operations - Resolves app freezing and performance degradation
This commit is contained in:
15
src/App.tsx
15
src/App.tsx
@@ -31,6 +31,7 @@ import type { ExportOptions } from './utils/exportImport';
|
||||
import { useAppState } from './hooks/useAppState';
|
||||
import { useSimulation } from './hooks/useSimulation';
|
||||
import { useLanguage } from './hooks/useLanguage';
|
||||
import { useWindowSize } from './hooks/useWindowSize';
|
||||
|
||||
// --- Main Component ---
|
||||
const MedPlanAssistant = () => {
|
||||
@@ -59,17 +60,9 @@ const MedPlanAssistant = () => {
|
||||
};
|
||||
|
||||
// Use shorter button labels on narrow screens to keep the pin control visible
|
||||
const [useCompactButtons, setUseCompactButtons] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const updateCompact = () => {
|
||||
setUseCompactButtons(window.innerWidth < 520); // tweakable threshold
|
||||
};
|
||||
|
||||
updateCompact();
|
||||
window.addEventListener('resize', updateCompact);
|
||||
return () => window.removeEventListener('resize', updateCompact);
|
||||
}, []);
|
||||
// Using debounced window size to prevent performance issues during resize
|
||||
const { width: windowWidth } = useWindowSize(150);
|
||||
const useCompactButtons = windowWidth < 520; // tweakable threshold
|
||||
|
||||
const {
|
||||
appState,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useWindowSize } from '../hooks/useWindowSize';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Label } from './ui/label';
|
||||
import { Switch } from './ui/switch';
|
||||
@@ -108,20 +109,9 @@ const Settings = ({
|
||||
const [therapeuticRangeError, setTherapeuticRangeError] = React.useState<string>('');
|
||||
const [yAxisRangeError, setYAxisRangeError] = React.useState<string>('');
|
||||
|
||||
// Track window width for responsive tooltip positioning
|
||||
const [isNarrowScreen, setIsNarrowScreen] = React.useState(
|
||||
typeof window !== 'undefined' ? window.innerWidth < 640 : false
|
||||
);
|
||||
|
||||
// Update narrow screen state on window resize
|
||||
React.useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsNarrowScreen(window.innerWidth < 640);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
// Track window width for responsive tooltip positioning using debounced hook
|
||||
const { width: windowWidth } = useWindowSize(150);
|
||||
const isNarrowScreen = windowWidth < 640;
|
||||
|
||||
// Determine tooltip side based on screen width
|
||||
const tooltipSide = isNarrowScreen ? 'top' : 'right';
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
TooltipTrigger as UiTooltipTrigger,
|
||||
TooltipContent as UiTooltipContent,
|
||||
} from './ui/tooltip';
|
||||
import { useElementSize } from '../hooks/useElementSize';
|
||||
|
||||
// Chart color scheme
|
||||
const CHART_COLORS = {
|
||||
@@ -70,21 +71,9 @@ const SimulationChart = ({
|
||||
const dispDays = parseInt(displayedDays, 10) || 2;
|
||||
const simDays = parseInt(simulationDays, 10) || 3;
|
||||
|
||||
// Calculate chart dimensions
|
||||
const [containerWidth, setContainerWidth] = React.useState(1000);
|
||||
// Calculate chart dimensions using debounced element size observer
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (containerRef.current) {
|
||||
setContainerWidth(containerRef.current.clientWidth);
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
window.addEventListener('resize', updateWidth);
|
||||
return () => window.removeEventListener('resize', updateWidth);
|
||||
}, []);
|
||||
const { width: containerWidth } = useElementSize(containerRef, 150);
|
||||
|
||||
// Track current theme for chart styling
|
||||
const [isDarkTheme, setIsDarkTheme] = React.useState(false);
|
||||
|
||||
33
src/hooks/useDebounce.ts
Normal file
33
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* useDebounce Hook
|
||||
*
|
||||
* Debounces a value to prevent excessive updates.
|
||||
* Useful for performance optimization with frequently changing values.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Debounces a value by delaying its update
|
||||
* @param value - The value to debounce
|
||||
* @param delay - Delay in milliseconds (default: 150ms)
|
||||
* @returns The debounced value
|
||||
*/
|
||||
export function useDebounce<T>(value: T, delay: number = 150): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
63
src/hooks/useElementSize.ts
Normal file
63
src/hooks/useElementSize.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* useElementSize Hook
|
||||
*
|
||||
* Tracks element dimensions using ResizeObserver with debouncing.
|
||||
* More efficient than window resize events for container-specific sizing.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { useEffect, useState, RefObject } from 'react';
|
||||
import { useDebounce } from './useDebounce';
|
||||
|
||||
interface ElementSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to track element size with debouncing
|
||||
* @param ref - React ref to the element to observe
|
||||
* @param debounceDelay - Delay in milliseconds for debouncing (default: 150ms)
|
||||
* @returns Current element dimensions (debounced)
|
||||
*/
|
||||
export function useElementSize<T extends HTMLElement>(
|
||||
ref: RefObject<T | null>,
|
||||
debounceDelay: number = 150
|
||||
): ElementSize {
|
||||
const [size, setSize] = useState<ElementSize>({
|
||||
width: 1000,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
// Debounce the size to prevent excessive re-renders
|
||||
const debouncedSize = useDebounce(size, debounceDelay);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
// Set initial size
|
||||
setSize({
|
||||
width: element.clientWidth,
|
||||
height: element.clientHeight,
|
||||
});
|
||||
|
||||
// Use ResizeObserver for efficient element size tracking
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setSize({ width, height });
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return debouncedSize;
|
||||
}
|
||||
46
src/hooks/useWindowSize.ts
Normal file
46
src/hooks/useWindowSize.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* useWindowSize Hook
|
||||
*
|
||||
* Tracks window dimensions with debouncing to prevent excessive re-renders
|
||||
* during window resize operations.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebounce } from './useDebounce';
|
||||
|
||||
interface WindowSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to track window size with debouncing
|
||||
* @param debounceDelay - Delay in milliseconds for debouncing (default: 150ms)
|
||||
* @returns Current window dimensions (debounced)
|
||||
*/
|
||||
export function useWindowSize(debounceDelay: number = 150): WindowSize {
|
||||
const [windowSize, setWindowSize] = useState<WindowSize>({
|
||||
width: typeof window !== 'undefined' ? window.innerWidth : 1000,
|
||||
height: typeof window !== 'undefined' ? window.innerHeight : 800,
|
||||
});
|
||||
|
||||
// Debounce the window size to prevent excessive re-renders
|
||||
const debouncedWindowSize = useDebounce(windowSize, debounceDelay);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return debouncedWindowSize;
|
||||
}
|
||||
Reference in New Issue
Block a user