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:
2026-02-09 17:19:43 +00:00
parent 7a2a8b0b47
commit 8325f10b19
6 changed files with 153 additions and 39 deletions

View File

@@ -31,6 +31,7 @@ import type { ExportOptions } from './utils/exportImport';
import { useAppState } from './hooks/useAppState'; import { useAppState } from './hooks/useAppState';
import { useSimulation } from './hooks/useSimulation'; import { useSimulation } from './hooks/useSimulation';
import { useLanguage } from './hooks/useLanguage'; import { useLanguage } from './hooks/useLanguage';
import { useWindowSize } from './hooks/useWindowSize';
// --- Main Component --- // --- Main Component ---
const MedPlanAssistant = () => { const MedPlanAssistant = () => {
@@ -59,17 +60,9 @@ const MedPlanAssistant = () => {
}; };
// Use shorter button labels on narrow screens to keep the pin control visible // Use shorter button labels on narrow screens to keep the pin control visible
const [useCompactButtons, setUseCompactButtons] = React.useState(false); // Using debounced window size to prevent performance issues during resize
const { width: windowWidth } = useWindowSize(150);
React.useEffect(() => { const useCompactButtons = windowWidth < 520; // tweakable threshold
const updateCompact = () => {
setUseCompactButtons(window.innerWidth < 520); // tweakable threshold
};
updateCompact();
window.addEventListener('resize', updateCompact);
return () => window.removeEventListener('resize', updateCompact);
}, []);
const { const {
appState, appState,

View File

@@ -10,6 +10,7 @@
*/ */
import React from 'react'; import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';
import { Card, CardContent } from './ui/card'; import { Card, CardContent } from './ui/card';
import { Label } from './ui/label'; import { Label } from './ui/label';
import { Switch } from './ui/switch'; import { Switch } from './ui/switch';
@@ -108,20 +109,9 @@ const Settings = ({
const [therapeuticRangeError, setTherapeuticRangeError] = React.useState<string>(''); const [therapeuticRangeError, setTherapeuticRangeError] = React.useState<string>('');
const [yAxisRangeError, setYAxisRangeError] = React.useState<string>(''); const [yAxisRangeError, setYAxisRangeError] = React.useState<string>('');
// Track window width for responsive tooltip positioning // Track window width for responsive tooltip positioning using debounced hook
const [isNarrowScreen, setIsNarrowScreen] = React.useState( const { width: windowWidth } = useWindowSize(150);
typeof window !== 'undefined' ? window.innerWidth < 640 : false const isNarrowScreen = windowWidth < 640;
);
// Update narrow screen state on window resize
React.useEffect(() => {
const handleResize = () => {
setIsNarrowScreen(window.innerWidth < 640);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Determine tooltip side based on screen width // Determine tooltip side based on screen width
const tooltipSide = isNarrowScreen ? 'top' : 'right'; const tooltipSide = isNarrowScreen ? 'top' : 'right';

View File

@@ -26,6 +26,7 @@ import {
TooltipTrigger as UiTooltipTrigger, TooltipTrigger as UiTooltipTrigger,
TooltipContent as UiTooltipContent, TooltipContent as UiTooltipContent,
} from './ui/tooltip'; } from './ui/tooltip';
import { useElementSize } from '../hooks/useElementSize';
// Chart color scheme // Chart color scheme
const CHART_COLORS = { const CHART_COLORS = {
@@ -70,21 +71,9 @@ const SimulationChart = ({
const dispDays = parseInt(displayedDays, 10) || 2; const dispDays = parseInt(displayedDays, 10) || 2;
const simDays = parseInt(simulationDays, 10) || 3; const simDays = parseInt(simulationDays, 10) || 3;
// Calculate chart dimensions // Calculate chart dimensions using debounced element size observer
const [containerWidth, setContainerWidth] = React.useState(1000);
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth } = useElementSize(containerRef, 150);
React.useEffect(() => {
const updateWidth = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.clientWidth);
}
};
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}, []);
// Track current theme for chart styling // Track current theme for chart styling
const [isDarkTheme, setIsDarkTheme] = React.useState(false); const [isDarkTheme, setIsDarkTheme] = React.useState(false);

33
src/hooks/useDebounce.ts Normal file
View 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;
}

View 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;
}

View 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;
}