[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,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) {