From 2f0eaf72df227dc21b48271a4b72ca0d41909b1c Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 19 Sep 2024 20:43:33 +0200 Subject: [PATCH] [Perf] Teact: Introduce `useUnmountCleanup` to reduce redundant effects --- src/components/common/AnimatedSticker.tsx | 22 ++++++++--------- .../hooks/useVideoWaitingSignal.ts | 11 ++++----- .../mediaViewer/hooks/useZoomChangeSignal.ts | 10 ++++---- src/hooks/data/useSelectorSignal.ts | 15 ++++++------ src/hooks/useCurrentTimeSignal.ts | 11 ++++----- src/hooks/useLongPress.ts | 10 ++++---- src/hooks/useSignalEffect.ts | 9 +++---- src/hooks/useSyncEffect.ts | 9 +++---- src/lib/teact/teact.ts | 24 +++++++++++++++++-- src/lib/teact/teactn.tsx | 10 ++++---- 10 files changed, 69 insertions(+), 62 deletions(-) diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 95c59da81..e357cb0b3 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -2,7 +2,11 @@ import type { RefObject } from 'react'; import type { FC } from '../../lib/teact/teact'; import React, { getIsHeavyAnimating, - memo, useEffect, useRef, useState, + memo, + useEffect, + useRef, + useState, + useUnmountCleanup, } from '../../lib/teact/teact'; import type RLottieInstance from '../../lib/rlottie/RLottie'; @@ -118,11 +122,9 @@ const AnimatedSticker: FC = ({ }, [color, shouldUseColorFilter]); const isUnmountedRef = useRef(false); - useEffect(() => { - return () => { - isUnmountedRef.current = true; - }; - }, []); + useUnmountCleanup(() => { + isUnmountedRef.current = true; + }); const init = useLastCallback(() => { if ( @@ -184,11 +186,9 @@ const AnimatedSticker: FC = ({ animation.setColor(rgbColor.current); }, [color, animation]); - useEffect(() => { - return () => { - animationRef.current?.removeView(viewId); - }; - }, [viewId]); + useUnmountCleanup(() => { + animationRef.current?.removeView(viewId); + }); const playAnimation = useLastCallback((shouldRestart = false) => { if ( diff --git a/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts b/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts index 64e86f018..f13835a7d 100644 --- a/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts +++ b/src/components/mediaViewer/hooks/useVideoWaitingSignal.ts @@ -1,14 +1,13 @@ -import { useEffect } from '../../../lib/teact/teact'; +import { useUnmountCleanup } from '../../../lib/teact/teact'; import { createSignal } from '../../../util/signals'; export const [getIsVideoWaiting, setIsVideoWaiting] = createSignal(false); export default function useVideoWaitingSignal() { - useEffect(() => { - return () => { - setIsVideoWaiting(false); - }; - }, []); + useUnmountCleanup(() => { + setIsVideoWaiting(false); + }); + return [getIsVideoWaiting, setIsVideoWaiting] as const; } diff --git a/src/components/mediaViewer/hooks/useZoomChangeSignal.ts b/src/components/mediaViewer/hooks/useZoomChangeSignal.ts index 6e1c75591..37648e1a8 100644 --- a/src/components/mediaViewer/hooks/useZoomChangeSignal.ts +++ b/src/components/mediaViewer/hooks/useZoomChangeSignal.ts @@ -1,15 +1,13 @@ -import { useEffect } from '../../../lib/teact/teact'; +import { useUnmountCleanup } from '../../../lib/teact/teact'; import { createSignal } from '../../../util/signals'; const [getZoomChange, setZoomChange] = createSignal(1); export default function useZoomChange() { - useEffect(() => { - return () => { - setZoomChange(1); - }; - }, []); + useUnmountCleanup(() => { + setZoomChange(1); + }); return [getZoomChange, setZoomChange] as const; } diff --git a/src/hooks/data/useSelectorSignal.ts b/src/hooks/data/useSelectorSignal.ts index f550a2275..061547f0f 100644 --- a/src/hooks/data/useSelectorSignal.ts +++ b/src/hooks/data/useSelectorSignal.ts @@ -5,7 +5,7 @@ import type { GlobalState } from '../../global/types'; import type { Signal, SignalSetter } from '../../util/signals'; import { createSignal } from '../../util/signals'; -import useEffectOnce from '../useEffectOnce'; +import useSyncEffect from '../useSyncEffect'; /* This hook is a more performant variation of the standard React `useSelector` hook. It allows to: @@ -31,24 +31,25 @@ addCallback((global: GlobalState) => { function useSelectorSignal(selector: Selector): Signal { let state = bySelector.get(selector); - if (!state) { const [getter, setter] = createSignal(selector(getGlobal())); state = { clientsCount: 0, getter, setter }; bySelector.set(selector, state); } - useEffectOnce(() => { - state!.clientsCount++; + useSyncEffect(() => { + const state2 = bySelector.get(selector)!; + + state2.clientsCount++; return () => { - state!.clientsCount--; + state2.clientsCount--; - if (!state!.clientsCount) { + if (!state2.clientsCount) { bySelector.delete(selector); } }; - }); + }, [selector]); return state.getter as Signal; } diff --git a/src/hooks/useCurrentTimeSignal.ts b/src/hooks/useCurrentTimeSignal.ts index a5d6f1979..892705e46 100644 --- a/src/hooks/useCurrentTimeSignal.ts +++ b/src/hooks/useCurrentTimeSignal.ts @@ -1,14 +1,13 @@ -import { useEffect } from '../lib/teact/teact'; +import { useUnmountCleanup } from '../lib/teact/teact'; import { createSignal } from '../util/signals'; export const [getCurrentTime, setCurrentTime] = createSignal(0); export default function useCurrentTimeSignal() { - useEffect(() => { - return () => { - setCurrentTime(0); - }; - }, []); + useUnmountCleanup(() => { + setCurrentTime(0); + }); + return [getCurrentTime, setCurrentTime] as const; } diff --git a/src/hooks/useLongPress.ts b/src/hooks/useLongPress.ts index 100619c99..cc78be115 100644 --- a/src/hooks/useLongPress.ts +++ b/src/hooks/useLongPress.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from '../lib/teact/teact'; +import { useCallback, useRef, useUnmountCleanup } from '../lib/teact/teact'; const DEFAULT_THRESHOLD = 250; @@ -41,11 +41,9 @@ function useLongPress({ window.clearTimeout(timerId.current); }, [onEnd, onClick]); - useEffect(() => { - return () => { - window.clearTimeout(timerId.current); - }; - }, []); + useUnmountCleanup(() => { + window.clearTimeout(timerId.current); + }); return { onMouseDown: start, diff --git a/src/hooks/useSignalEffect.ts b/src/hooks/useSignalEffect.ts index 13c2db02c..2018fbfd3 100644 --- a/src/hooks/useSignalEffect.ts +++ b/src/hooks/useSignalEffect.ts @@ -1,7 +1,6 @@ -import { useRef } from '../lib/teact/teact'; +import { useRef, useUnmountCleanup } from '../lib/teact/teact'; import { cleanupEffect, isSignal } from '../util/signals'; -import useEffectOnce from './useEffectOnce'; export function useSignalEffect(effect: NoneToVoidFunction, dependencies: readonly any[]) { // The is extracted from `useEffectOnce` to run before all effects @@ -16,9 +15,7 @@ export function useSignalEffect(effect: NoneToVoidFunction, dependencies: readon }); } - useEffectOnce(() => { - return () => { - cleanupEffect(effect); - }; + useUnmountCleanup(() => { + cleanupEffect(effect); }); } diff --git a/src/hooks/useSyncEffect.ts b/src/hooks/useSyncEffect.ts index 6a8437470..2a232c60e 100644 --- a/src/hooks/useSyncEffect.ts +++ b/src/hooks/useSyncEffect.ts @@ -1,6 +1,5 @@ -import { useRef } from '../lib/teact/teact'; +import { useRef, useUnmountCleanup } from '../lib/teact/teact'; -import useEffectOnce from './useEffectOnce'; import usePreviousDeprecated from './usePreviousDeprecated'; export default function useSyncEffect( @@ -15,9 +14,7 @@ export default function useSyncEffect( cleanupRef.current = effect(prevDeps || []) ?? undefined; } - useEffectOnce(() => { - return () => { - cleanupRef.current?.(); - }; + useUnmountCleanup(() => { + cleanupRef.current?.(); }); } diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 864dce4b6..2de305d8b 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -92,7 +92,7 @@ interface ComponentInstance { cursor: number; byCursor: { dependencies?: readonly any[]; - schedule: NoneToVoidFunction; + schedule?: NoneToVoidFunction; cleanup?: NoneToVoidFunction; releaseSignals?: NoneToVoidFunction; }[]; @@ -778,7 +778,7 @@ function useEffectBase( console.log(`[Teact] Effect "${debugKey}" caused by signal #${i} new value:`, signal()); } - byCursor[cursor].schedule(); + byCursor[cursor].schedule!(); })); if (!cleanups?.length) { @@ -807,6 +807,26 @@ export function useLayoutEffect(effect: Effect, dependencies?: readonly any[], d return useEffectBase(true, effect, dependencies, debugKey); } +export function useUnmountCleanup(cleanup: NoneToVoidFunction) { + if (!renderingInstance.hooks) { + renderingInstance.hooks = {}; + } + + if (!renderingInstance.hooks.effects) { + renderingInstance.hooks.effects = { cursor: 0, byCursor: [] }; + } + + const { cursor, byCursor } = renderingInstance.hooks.effects; + + if (!byCursor[cursor]) { + byCursor[cursor] = { + cleanup, + }; + } + + renderingInstance.hooks.effects.cursor++; +} + export function useMemo( resolver: () => T, dependencies: any[], diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index c1eac1f9b..5c9d37c9f 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -6,7 +6,7 @@ import { handleError } from '../../util/handleError'; import { orderBy } from '../../util/iteratees'; import { throttleWithTickEnd } from '../../util/schedulers'; import { requestMeasure } from '../fasterdom/fasterdom'; -import React, { DEBUG_resolveComponentName, getIsHeavyAnimating, useEffect } from './teact'; +import React, { DEBUG_resolveComponentName, getIsHeavyAnimating, useUnmountCleanup } from './teact'; import useForceUpdate from '../../hooks/useForceUpdate'; import useUniqueId from '../../hooks/useUniqueId'; @@ -255,11 +255,9 @@ export function withGlobal( const id = useUniqueId(); const forceUpdate = useForceUpdate(); - useEffect(() => { - return () => { - containers.delete(id); - }; - }, [id]); + useUnmountCleanup(() => { + containers.delete(id); + }); let container = containers.get(id)!; if (!container) {