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 { 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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
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