diff --git a/src/api/useDebounceIfVisible.ts b/src/api/useDebounceIfVisible.ts new file mode 100644 index 00000000..a1a7f3c7 --- /dev/null +++ b/src/api/useDebounceIfVisible.ts @@ -0,0 +1,101 @@ +import { getDefaultStore } from "jotai"; +import { useCallback, useMemo, useRef } from "react"; +import { useUnmount } from "react-use"; +import { useThrottledCallback, useDebouncedCallback } from "use-debounce"; +import { isDocumentVisibleAtom } from "../atoms"; + +const store = getDefaultStore(); + +/** + * Only use the throttled callback while visible. + * Otherwise, clear the timers and use requestAnimationFrame to call + * the callback on the next frame (rAF is paused while backgrounded, so no timers + * accumulate). + */ +export function useThrottledCallbackIfVisible< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends (...args: any) => ReturnType, +>(...args: Parameters>) { + const throttledFn = useThrottledCallback(...args); + const rafRef = useRef(null); + + const argFunc = args[0]; + const funcRef = useRef(argFunc); + funcRef.current = argFunc; + + useUnmount(() => { + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + }); + + const fn = useCallback( + (...args: Parameters) => { + if (store.get(isDocumentVisibleAtom)) { + throttledFn(...args); + return; + } + + // hidden, don't accumulate timers from throttledFn + throttledFn.cancel(); + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => { + return funcRef.current(...args); + }); + }, + [throttledFn], + ); + + const cancel = useCallback(() => { + throttledFn.cancel(); + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + }, [throttledFn]); + + return useMemo(() => Object.assign(fn, { cancel }), [fn, cancel]); +} + +/** + * Only use the debounced callback while visible. + * Otherwise, clear the timers and use requestAnimationFrame to call + * the callback on the next frame (rAF is paused while backgrounded, so no timers + * accumulate). + */ +export function useDebouncedCallbackIfVisible< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends (...args: any) => ReturnType, +>(...args: Parameters>) { + const debouncedFn = useDebouncedCallback(...args); + const rafRef = useRef(null); + + const argFunc = args[0]; + const funcRef = useRef(argFunc); + funcRef.current = argFunc; + + useUnmount(() => { + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + }); + + const fn = useCallback( + (...args: Parameters) => { + if (store.get(isDocumentVisibleAtom)) { + debouncedFn(...args); + return; + } + + // hidden, don't accumulate timers from debouncedFn + debouncedFn.cancel(); + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(() => { + return funcRef.current(...args); + }); + }, + [debouncedFn], + ); + + const cancel = useCallback(() => { + debouncedFn.cancel(); + if (rafRef.current != null) cancelAnimationFrame(rafRef.current); + rafRef.current = null; + }, [debouncedFn]); + + return useMemo(() => Object.assign(fn, { cancel }), [fn, cancel]); +} diff --git a/src/api/useSetAtomWsData.ts b/src/api/useSetAtomWsData.ts index d46055bb..ebebd46a 100644 --- a/src/api/useSetAtomWsData.ts +++ b/src/api/useSetAtomWsData.ts @@ -11,7 +11,10 @@ import type { import { isEmaObjectKey } from "./worker/types"; import { DateTime } from "luxon"; import { useInterval } from "react-use"; -import { useThrottledCallback, useDebouncedCallback } from "use-debounce"; +import { + useThrottledCallbackIfVisible, + useDebouncedCallbackIfVisible, +} from "./useDebounceIfVisible"; import type z from "zod"; import { skipRateAtom, @@ -241,18 +244,21 @@ function useUpdateAtoms() { useInterval(updateSlotDurationDbMs, 1_000); const setEstimatedSlotDuration = useSetAtom(estimatedSlotDurationAtom); - const setDbEstimatedSlotDuration = useThrottledCallback( + const setDbEstimatedSlotDuration = useThrottledCallbackIfVisible( (value?: EstimatedSlotDuration) => setEstimatedSlotDuration(value), slotDurationDbMs, ); const setEstimatedTps = useSetAtom(estimatedTpsAtom); - const setDbEstimatedTps = useThrottledCallback((value?: EstimatedTps) => { - setEstimatedTps(value); - }, estimatedTpsDebounceMs); + const setDbEstimatedTps = useThrottledCallbackIfVisible( + (value?: EstimatedTps) => { + setEstimatedTps(value); + }, + estimatedTpsDebounceMs, + ); const setLiveNetworkMetrics = useSetAtom(liveNetworkMetricsAtom); - const setDbLiveNetworkMetrics = useThrottledCallback( + const setDbLiveNetworkMetrics = useThrottledCallbackIfVisible( (value?: LiveNetworkMetrics) => { setLiveNetworkMetrics(value); }, @@ -260,12 +266,15 @@ function useUpdateAtoms() { ); const setLiveTileMetrics = useSetAtom(liveTileMetricsAtom); - const setDbLiveTileMetrics = useThrottledCallback((value?: TileMetrics) => { - setLiveTileMetrics(value); - }, liveTileMetricsDebounceMs); + const setDbLiveTileMetrics = useThrottledCallbackIfVisible( + (value?: TileMetrics) => { + setLiveTileMetrics(value); + }, + liveTileMetricsDebounceMs, + ); const setLivePrimaryMetrics = useSetAtom(liveTilePrimaryMetricAtom); - const setDbLivePrimaryMetrics = useThrottledCallback( + const setDbLivePrimaryMetrics = useThrottledCallbackIfVisible( (value?: LiveTilePrimaryMetric) => { setLivePrimaryMetrics(value); }, @@ -274,7 +283,7 @@ function useUpdateAtoms() { const setRateLiveTxnWaterfall = useSetAtom(rateLiveWaterfallAtom); const setLiveTxnWaterfall = useSetAtom(liveTxnWaterfallAtom); - const setDbLiveTxnWaterfall = useThrottledCallback( + const setDbLiveTxnWaterfall = useThrottledCallbackIfVisible( (value?: LiveTxnWaterfall) => { setLiveTxnWaterfall(value); setRateLiveTxnWaterfall(value?.waterfall); @@ -283,7 +292,7 @@ function useUpdateAtoms() { ); const setTileTimer = useSetAtom(tileTimerAtom); - const setDbTileTimer = useThrottledCallback((value?: number[]) => { + const setDbTileTimer = useThrottledCallbackIfVisible((value?: number[]) => { setTileTimer(value); }, tileTimerDebounceMs); @@ -307,7 +316,7 @@ function useUpdateAtoms() { const setSlotStatus = useSetAtom(setSlotStatusAtom); const setGossipNetworkStats = useSetAtom(gossipNetworkStatsAtom); - const setDbGossipNetworkStats = useThrottledCallback( + const setDbGossipNetworkStats = useThrottledCallbackIfVisible( (value?: GossipNetworkStats) => { setGossipNetworkStats(value); }, @@ -315,7 +324,7 @@ function useUpdateAtoms() { ); const setGossipPeersSize = useSetAtom(gossipPeersSizeAtom); - const setDbGossipPeersSize = useThrottledCallback( + const setDbGossipPeersSize = useThrottledCallbackIfVisible( (value?: GossipPeersSize) => { setGossipPeersSize(value); }, @@ -427,7 +436,7 @@ function useUpdateAtoms() { const peersBuffer = useRef(new Map()); const removePeersBuffer = useRef(new Map()); - const dbFlushBuffer = useDebouncedCallback( + const dbFlushBuffer = useDebouncedCallbackIfVisible( () => { updatePeers([...peersBuffer.current.values()]); removePeers([...removePeersBuffer.current.values()]); @@ -459,6 +468,7 @@ function useUpdateAtoms() { } } + // only triggers when document is visible dbFlushBuffer(); }, [dbFlushBuffer], @@ -472,7 +482,7 @@ function useUpdateAtoms() { toRemove: new Set(), }); - const dbFlushSupermajorityPeersBuffers = useDebouncedCallback( + const dbFlushSupermajorityPeersBuffers = useDebouncedCallbackIfVisible( () => { updateSupermajorityOnlinePeers( [...supermajorityPeersBuffers.current.toAdd], @@ -499,6 +509,7 @@ function useUpdateAtoms() { } } + // only triggers when document is visible dbFlushSupermajorityPeersBuffers(); }, [dbFlushSupermajorityPeersBuffers],