Teact: Avoid multiple renders in a single frame

This commit is contained in:
Alexander Zinchuk 2023-02-08 00:43:27 +01:00
parent 9c25abbd9a
commit 31e52956c2

View File

@ -5,7 +5,6 @@ import {
fastRafPrimaryWithFallback, fastRafPrimaryWithFallback,
onTickEnd, onTickEnd,
onTickEndPrimary, onTickEndPrimary,
throttleWithPrimaryRafFallback,
throttleWithRafFallback, throttleWithRafFallback,
} from '../../util/schedulers'; } from '../../util/schedulers';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
@ -61,6 +60,7 @@ export interface VirtualElementFragment {
export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void; export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void;
interface ComponentInstance { interface ComponentInstance {
id: number;
$element: VirtualElementComponent; $element: VirtualElementComponent;
Component: FC; Component: FC;
name: string; name: string;
@ -130,6 +130,7 @@ const DEBUG_RENDER_THRESHOLD = 7;
const DEBUG_EFFECT_THRESHOLD = 7; const DEBUG_EFFECT_THRESHOLD = 7;
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']); const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
let lastComponentId = 0;
let renderingInstance: ComponentInstance; let renderingInstance: ComponentInstance;
export function isEmptyElement($element: VirtualElement): $element is VirtualElementEmpty { export function isEmptyElement($element: VirtualElement): $element is VirtualElementEmpty {
@ -190,6 +191,7 @@ function createComponentInstance(Component: FC, props: Props, children: any[]):
} }
const componentInstance: ComponentInstance = { const componentInstance: ComponentInstance = {
id: ++lastComponentId,
$element: {} as VirtualElementComponent, $element: {} as VirtualElementComponent,
Component, Component,
name: Component.name, name: Component.name,
@ -292,14 +294,46 @@ function buildEmptyElement(): VirtualElementEmpty {
} }
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
const DEBUG_components: AnyLiteral = {}; const DEBUG_components: AnyLiteral = { TOTAL: { componentName: 'TOTAL', renderCount: 0 } };
document.addEventListener('dblclick', () => { document.addEventListener('dblclick', () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc')); console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc'));
}); });
let instancesPendingUpdate = new Set<ComponentInstance>();
let idsToExcludeFromUpdate = new Set<number>();
const runUpdatePassOnRaf = throttleWithRafFallback(() => {
idsToExcludeFromUpdate = new Set();
const instancesToUpdate = Array
.from(instancesPendingUpdate)
.sort((a, b) => a.id - b.id);
instancesPendingUpdate = new Set();
instancesToUpdate.forEach((instance) => {
prepareComponentForFrame(instance);
});
instancesToUpdate.forEach((instance) => {
if (idsToExcludeFromUpdate!.has(instance.id)) {
return;
}
forceUpdateComponent(instance);
});
});
function scheduleUpdate(componentInstance: ComponentInstance) {
instancesPendingUpdate.add(componentInstance);
runUpdatePassOnRaf();
}
export function renderComponent(componentInstance: ComponentInstance) { export function renderComponent(componentInstance: ComponentInstance) {
idsToExcludeFromUpdate.add(componentInstance.id);
renderingInstance = componentInstance; renderingInstance = componentInstance;
componentInstance.hooks.state.cursor = 0; componentInstance.hooks.state.cursor = 0;
componentInstance.hooks.effects.cursor = 0; componentInstance.hooks.effects.cursor = 0;
@ -343,6 +377,7 @@ export function renderComponent(componentInstance: ComponentInstance) {
} }
DEBUG_components[componentName].renderTimes.push(duration); DEBUG_components[componentName].renderTimes.push(duration);
DEBUG_components[componentName].renderCount++; DEBUG_components[componentName].renderCount++;
DEBUG_components.TOTAL.renderCount++;
if (DEBUG_MORE) { if (DEBUG_MORE) {
incrementOverlayCounter(`${componentName} renders`); incrementOverlayCounter(`${componentName} renders`);
@ -398,6 +433,8 @@ export function unmountComponent(componentInstance: ComponentInstance) {
return; return;
} }
idsToExcludeFromUpdate.add(componentInstance.id);
componentInstance.hooks.effects.byCursor.forEach((effect) => { componentInstance.hooks.effects.byCursor.forEach((effect) => {
if (effect.cleanup) { if (effect.cleanup) {
try { try {
@ -443,7 +480,6 @@ function helpGc(componentInstance: ComponentInstance) {
componentInstance.renderedValue = undefined; componentInstance.renderedValue = undefined;
componentInstance.Component = undefined as any; componentInstance.Component = undefined as any;
componentInstance.props = undefined as any; componentInstance.props = undefined as any;
componentInstance.forceUpdate = undefined;
componentInstance.onUpdate = undefined; componentInstance.onUpdate = undefined;
} }
@ -455,9 +491,6 @@ function prepareComponentForFrame(componentInstance: ComponentInstance) {
componentInstance.hooks.state.byCursor.forEach((hook) => { componentInstance.hooks.state.byCursor.forEach((hook) => {
hook.value = hook.nextValue; hook.value = hook.nextValue;
}); });
componentInstance.prepareForFrame = throttleWithPrimaryRafFallback(() => prepareComponentForFrame(componentInstance));
componentInstance.forceUpdate = throttleWithRafFallback(() => forceUpdateComponent(componentInstance));
} }
function forceUpdateComponent(componentInstance: ComponentInstance) { function forceUpdateComponent(componentInstance: ComponentInstance) {
@ -492,17 +525,7 @@ export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter
byCursor[cursor].nextValue = newValue; byCursor[cursor].nextValue = newValue;
if (!componentInstance.prepareForFrame || !componentInstance.forceUpdate) { scheduleUpdate(componentInstance);
componentInstance.prepareForFrame = throttleWithPrimaryRafFallback(
() => prepareComponentForFrame(componentInstance),
);
componentInstance.forceUpdate = throttleWithRafFallback(
() => forceUpdateComponent(componentInstance),
);
}
componentInstance.prepareForFrame();
componentInstance.forceUpdate();
if (DEBUG_MORE) { if (DEBUG_MORE) {
if (componentInstance.name !== 'TeactNContainer') { if (componentInstance.name !== 'TeactNContainer') {