[Perf] Teact: Avoid scheduling redundant effect cleanups, refactoring

This commit is contained in:
Alexander Zinchuk 2024-09-19 20:43:36 +02:00
parent 2f0eaf72df
commit f503841d36

View File

@ -655,19 +655,94 @@ function useEffectBase(
if (!renderingInstance.hooks) { if (!renderingInstance.hooks) {
renderingInstance.hooks = {}; renderingInstance.hooks = {};
} }
if (!renderingInstance.hooks.effects) { if (!renderingInstance.hooks.effects) {
renderingInstance.hooks.effects = { cursor: 0, byCursor: [] }; renderingInstance.hooks.effects = { cursor: 0, byCursor: [] };
} }
const { cursor, byCursor } = renderingInstance.hooks.effects; const { cursor, byCursor } = renderingInstance.hooks.effects;
const effectConfig = byCursor[cursor];
const componentInstance = renderingInstance; const componentInstance = renderingInstance;
const runEffectCleanup = () => safeExec(() => { function schedule() {
const { cleanup } = byCursor[cursor]; scheduleEffect(componentInstance, cursor, effect, isLayout);
if (!cleanup) {
return;
} }
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}`);
}
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();
}
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 // eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_startAt: number | undefined; let DEBUG_startAt: number | undefined;
if (DEBUG) { if (DEBUG) {
@ -686,6 +761,8 @@ function useEffectBase(
); );
} }
} }
return undefined;
}, () => { }, () => {
// eslint-disable-next-line no-console, max-len // eslint-disable-next-line no-console, max-len
console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`, componentInstance); console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`, componentInstance);
@ -693,6 +770,9 @@ function useEffectBase(
byCursor[cursor].cleanup = undefined; byCursor[cursor].cleanup = undefined;
}); });
cleanupsContainer.set(effectId, runEffectCleanup);
}
const runEffect = () => safeExec(() => { const runEffect = () => safeExec(() => {
if (componentInstance.mountState === MountState.Unmounted) { if (componentInstance.mountState === MountState.Unmounted) {
return; return;
@ -722,83 +802,11 @@ function useEffectBase(
console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`, componentInstance); console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`, componentInstance);
}); });
function schedule() { effectsContainer.set(effectId, runEffect);
const effectId = `${componentInstance.id}_${cursor}`;
if (isLayout) {
pendingLayoutCleanups.set(effectId, runEffectCleanup);
pendingLayoutEffects.set(effectId, runEffect);
} else {
pendingCleanups.set(effectId, runEffectCleanup);
pendingEffects.set(effectId, runEffect);
}
runUpdatePassOnRaf(); 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++;
}
export function useEffect(effect: Effect, dependencies?: readonly any[], debugKey?: string) { export function useEffect(effect: Effect, dependencies?: readonly any[], debugKey?: string) {
return useEffectBase(false, effect, dependencies, debugKey); return useEffectBase(false, effect, dependencies, debugKey);
} }