From f503841d3643382b0b8bedabf5e6e4ff65465233 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 19 Sep 2024 20:43:36 +0200 Subject: [PATCH] [Perf] Teact: Avoid scheduling redundant effect cleanups, refactoring --- src/lib/teact/teact.ts | 204 +++++++++++++++++++++-------------------- 1 file changed, 106 insertions(+), 98 deletions(-) diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 2de305d8b..d2783e8b8 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -655,43 +655,123 @@ function useEffectBase( if (!renderingInstance.hooks) { renderingInstance.hooks = {}; } + if (!renderingInstance.hooks.effects) { renderingInstance.hooks.effects = { cursor: 0, byCursor: [] }; } const { cursor, byCursor } = renderingInstance.hooks.effects; + const effectConfig = byCursor[cursor]; const componentInstance = renderingInstance; - const runEffectCleanup = () => safeExec(() => { - const { cleanup } = byCursor[cursor]; - if (!cleanup) { - return; - } + function schedule() { + scheduleEffect(componentInstance, cursor, effect, isLayout); + } - // eslint-disable-next-line @typescript-eslint/naming-convention - let DEBUG_startAt: number | undefined; - if (DEBUG) { - DEBUG_startAt = performance.now(); - } + if (dependencies && effectConfig?.dependencies) { + if (dependencies.some((dependency, i) => dependency !== effectConfig.dependencies![i])) { + if (DEBUG && debugKey) { + const causedBy = dependencies.reduce((res, newValue, i) => { + const prevValue = effectConfig.dependencies![i]; + if (newValue !== prevValue) { + res.push(`${i}: ${prevValue} => ${newValue}`); + } - cleanup(); + return res; + }, []); - if (DEBUG) { - const duration = performance.now() - DEBUG_startAt!; - const componentName = DEBUG_resolveComponentName(componentInstance.Component); - if (duration > DEBUG_EFFECT_THRESHOLD) { // eslint-disable-next-line no-console - console.warn( - `[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`, - ); + console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', ')); } + + schedule(); } - }, () => { - // eslint-disable-next-line no-console, max-len - console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`, componentInstance); - }, () => { - byCursor[cursor].cleanup = undefined; - }); + } else { + if (debugKey) { + // eslint-disable-next-line no-console + console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`); + } + + schedule(); + } + + function setupSignals() { + const cleanups = dependencies?.filter(isSignal).map((signal, i) => signal.subscribe(() => { + if (debugKey) { + // eslint-disable-next-line no-console + console.log(`[Teact] Effect "${debugKey}" caused by signal #${i} new value:`, signal()); + } + + byCursor[cursor].schedule!(); + })); + + if (!cleanups?.length) { + return undefined; + } + + return () => { + for (const cleanup of cleanups) { + cleanup(); + } + }; + } + + byCursor[cursor] = { + ...effectConfig, + dependencies, + schedule, + }; + + if (!effectConfig) { + byCursor[cursor].releaseSignals = setupSignals(); + } + + renderingInstance.hooks.effects.cursor++; +} + +function scheduleEffect( + componentInstance: ComponentInstance, + cursor: number, + effect: Effect, + isLayout: boolean, +) { + const { byCursor } = componentInstance.hooks!.effects!; + const cleanup = byCursor[cursor]?.cleanup; + const cleanupsContainer = isLayout ? pendingLayoutCleanups : pendingCleanups; + const effectsContainer = isLayout ? pendingLayoutEffects : pendingEffects; + const effectId = `${componentInstance.id}_${cursor}`; + + if (cleanup) { + const runEffectCleanup = () => safeExec(() => { + // eslint-disable-next-line @typescript-eslint/naming-convention + let DEBUG_startAt: number | undefined; + if (DEBUG) { + DEBUG_startAt = performance.now(); + } + + cleanup(); + + if (DEBUG) { + const duration = performance.now() - DEBUG_startAt!; + const componentName = DEBUG_resolveComponentName(componentInstance.Component); + if (duration > DEBUG_EFFECT_THRESHOLD) { + // eslint-disable-next-line no-console + console.warn( + `[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`, + ); + } + } + + return undefined; + }, () => { + // eslint-disable-next-line no-console, max-len + console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`, componentInstance); + }, () => { + byCursor[cursor].cleanup = undefined; + }); + + cleanupsContainer.set(effectId, runEffectCleanup); + } const runEffect = () => safeExec(() => { if (componentInstance.mountState === MountState.Unmounted) { @@ -722,81 +802,9 @@ function useEffectBase( console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`, componentInstance); }); - function schedule() { - const effectId = `${componentInstance.id}_${cursor}`; + effectsContainer.set(effectId, runEffect); - if (isLayout) { - pendingLayoutCleanups.set(effectId, runEffectCleanup); - pendingLayoutEffects.set(effectId, runEffect); - } else { - pendingCleanups.set(effectId, runEffectCleanup); - pendingEffects.set(effectId, runEffect); - } - - runUpdatePassOnRaf(); - } - - if (dependencies && byCursor[cursor]?.dependencies) { - if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) { - if (DEBUG && debugKey) { - const causedBy = dependencies.reduce((res, newValue, i) => { - const prevValue = byCursor[cursor].dependencies![i]; - if (newValue !== prevValue) { - res.push(`${i}: ${prevValue} => ${newValue}`); - } - - return res; - }, []); - - // eslint-disable-next-line no-console - console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', ')); - } - - schedule(); - } - } else { - if (debugKey) { - // eslint-disable-next-line no-console - console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`); - } - - schedule(); - } - - const isFirstRun = !byCursor[cursor]; - - byCursor[cursor] = { - ...byCursor[cursor], - dependencies, - schedule, - }; - - function setupSignals() { - const cleanups = dependencies?.filter(isSignal).map((signal, i) => signal.subscribe(() => { - if (debugKey) { - // eslint-disable-next-line no-console - console.log(`[Teact] Effect "${debugKey}" caused by signal #${i} new value:`, signal()); - } - - byCursor[cursor].schedule!(); - })); - - if (!cleanups?.length) { - return undefined; - } - - return () => { - for (const cleanup of cleanups) { - cleanup(); - } - }; - } - - if (isFirstRun) { - byCursor[cursor].releaseSignals = setupSignals(); - } - - renderingInstance.hooks.effects.cursor++; + runUpdatePassOnRaf(); } export function useEffect(effect: Effect, dependencies?: readonly any[], debugKey?: string) {