Fix performance bug (#984)
This commit is contained in:
parent
f4cc4253b8
commit
e9f5175f51
@ -97,6 +97,16 @@ import { ScrollBars } from "../scene/types";
|
|||||||
import { generateCollaborationLink, getCollaborationLinkData } from "../data";
|
import { generateCollaborationLink, getCollaborationLinkData } from "../data";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
import { SceneStateCallbackRemover } from "../scene/globalScene";
|
||||||
|
|
||||||
|
function withBatchedUpdates<
|
||||||
|
TFunction extends ((event: any) => void) | (() => void)
|
||||||
|
>(func: TFunction) {
|
||||||
|
return (event => {
|
||||||
|
unstable_batchedUpdates(func, event);
|
||||||
|
}) as TFunction;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// TEST HOOKS
|
// TEST HOOKS
|
||||||
@ -159,6 +169,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
roomID: string | null = null;
|
roomID: string | null = null;
|
||||||
roomKey: string | null = null;
|
roomKey: string | null = null;
|
||||||
lastBroadcastedOrReceivedSceneVersion: number = -1;
|
lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
|
removeSceneCallback: SceneStateCallbackRemover | null = null;
|
||||||
|
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
canvasOnlyActions = ["selectAll"];
|
canvasOnlyActions = ["selectAll"];
|
||||||
@ -201,7 +212,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCut = (event: ClipboardEvent) => {
|
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||||
if (isWritableElement(event.target)) {
|
if (isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -214,20 +225,21 @@ export class App extends React.Component<any, AppState> {
|
|||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
this.setState({ ...appState });
|
this.setState({ ...appState });
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
});
|
||||||
private onCopy = (event: ClipboardEvent) => {
|
|
||||||
|
private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
|
||||||
if (isWritableElement(event.target)) {
|
if (isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
});
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = withBatchedUpdates(() => {
|
||||||
isHoldingSpace = false;
|
isHoldingSpace = false;
|
||||||
this.saveDebounced();
|
this.saveDebounced();
|
||||||
this.saveDebounced.flush();
|
this.saveDebounced.flush();
|
||||||
};
|
});
|
||||||
|
|
||||||
private disableEvent: EventHandlerNonNull = event => {
|
private disableEvent: EventHandlerNonNull = event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -454,7 +466,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSceneCallback = () => {
|
private onSceneUpdated = () => {
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -469,7 +481,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSceneState.addCallback(this.handleSceneCallback);
|
this.removeSceneCallback = globalSceneState.addCallback(
|
||||||
|
this.onSceneUpdated,
|
||||||
|
);
|
||||||
|
|
||||||
document.addEventListener("copy", this.onCopy);
|
document.addEventListener("copy", this.onCopy);
|
||||||
document.addEventListener("paste", this.pasteFromClipboard);
|
document.addEventListener("paste", this.pasteFromClipboard);
|
||||||
@ -528,6 +542,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
this.removeSceneCallback!();
|
||||||
|
|
||||||
document.removeEventListener("copy", this.onCopy);
|
document.removeEventListener("copy", this.onCopy);
|
||||||
document.removeEventListener("paste", this.pasteFromClipboard);
|
document.removeEventListener("paste", this.pasteFromClipboard);
|
||||||
document.removeEventListener("cut", this.onCut);
|
document.removeEventListener("cut", this.onCut);
|
||||||
@ -561,19 +577,21 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
public state: AppState = getDefaultAppState();
|
public state: AppState = getDefaultAppState();
|
||||||
|
|
||||||
private onResize = () => {
|
private onResize = withBatchedUpdates(() => {
|
||||||
globalSceneState
|
globalSceneState
|
||||||
.getAllElements()
|
.getAllElements()
|
||||||
.forEach(element => invalidateShapeForElement(element));
|
.forEach(element => invalidateShapeForElement(element));
|
||||||
this.setState({});
|
this.setState({});
|
||||||
};
|
});
|
||||||
|
|
||||||
private updateCurrentCursorPosition = (event: MouseEvent) => {
|
private updateCurrentCursorPosition = withBatchedUpdates(
|
||||||
|
(event: MouseEvent) => {
|
||||||
cursorX = event.x;
|
cursorX = event.x;
|
||||||
cursorY = event.y;
|
cursorY = event.y;
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
||||||
private onKeyDown = (event: KeyboardEvent) => {
|
private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
||||||
// case: using arrows to move between buttons
|
// case: using arrows to move between buttons
|
||||||
@ -629,9 +647,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
isHoldingSpace = true;
|
isHoldingSpace = true;
|
||||||
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
private onKeyUp = (event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
@ -644,7 +662,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
isHoldingSpace = false;
|
isHoldingSpace = false;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
private copyToAppClipboard = () => {
|
private copyToAppClipboard = () => {
|
||||||
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
copyToAppClipboard(globalSceneState.getAllElements(), this.state);
|
||||||
@ -666,7 +684,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private pasteFromClipboard = async (event: ClipboardEvent | null) => {
|
private pasteFromClipboard = withBatchedUpdates(
|
||||||
|
async (event: ClipboardEvent | null) => {
|
||||||
// #686
|
// #686
|
||||||
const target = document.activeElement;
|
const target = document.activeElement;
|
||||||
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
||||||
@ -714,7 +733,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.selectShapeTool("selection");
|
this.selectShapeTool("selection");
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
||||||
private selectShapeTool(elementType: AppState["elementType"]) {
|
private selectShapeTool(elementType: AppState["elementType"]) {
|
||||||
if (!isHoldingSpace) {
|
if (!isHoldingSpace) {
|
||||||
@ -730,21 +750,23 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onGestureStart = (event: GestureEvent) => {
|
private onGestureStart = withBatchedUpdates((event: GestureEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
gesture.initialScale = this.state.zoom;
|
gesture.initialScale = this.state.zoom;
|
||||||
};
|
});
|
||||||
private onGestureChange = (event: GestureEvent) => {
|
|
||||||
|
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
zoom: getNormalizedZoom(gesture.initialScale! * event.scale),
|
zoom: getNormalizedZoom(gesture.initialScale! * event.scale),
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
private onGestureEnd = (event: GestureEvent) => {
|
|
||||||
|
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
gesture.initialScale = null;
|
gesture.initialScale = null;
|
||||||
};
|
});
|
||||||
|
|
||||||
setAppState = (obj: any) => {
|
setAppState = (obj: any) => {
|
||||||
this.setState(obj);
|
this.setState(obj);
|
||||||
@ -1174,7 +1196,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
isPanning = true;
|
isPanning = true;
|
||||||
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
||||||
let { clientX: lastX, clientY: lastY } = event;
|
let { clientX: lastX, clientY: lastY } = event;
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
||||||
const deltaX = lastX - event.clientX;
|
const deltaX = lastX - event.clientX;
|
||||||
const deltaY = lastY - event.clientY;
|
const deltaY = lastY - event.clientY;
|
||||||
lastX = event.clientX;
|
lastX = event.clientX;
|
||||||
@ -1188,8 +1210,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.state.scrollY - deltaY / this.state.zoom,
|
this.state.scrollY - deltaY / this.state.zoom,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
const teardown = (lastPointerUp = () => {
|
const teardown = withBatchedUpdates(
|
||||||
|
(lastPointerUp = () => {
|
||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
isPanning = false;
|
isPanning = false;
|
||||||
if (!isHoldingSpace) {
|
if (!isHoldingSpace) {
|
||||||
@ -1198,7 +1221,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.removeEventListener("pointermove", onPointerMove);
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
window.removeEventListener("pointerup", teardown);
|
window.removeEventListener("pointerup", teardown);
|
||||||
window.removeEventListener("blur", teardown);
|
window.removeEventListener("blur", teardown);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
window.addEventListener("blur", teardown);
|
window.addEventListener("blur", teardown);
|
||||||
window.addEventListener("pointermove", onPointerMove, {
|
window.addEventListener("pointermove", onPointerMove, {
|
||||||
passive: true,
|
passive: true,
|
||||||
@ -1264,7 +1288,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
isDraggingScrollBar = true;
|
isDraggingScrollBar = true;
|
||||||
lastX = event.clientX;
|
lastX = event.clientX;
|
||||||
lastY = event.clientY;
|
lastY = event.clientY;
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@ -1288,15 +1312,15 @@ export class App extends React.Component<any, AppState> {
|
|||||||
});
|
});
|
||||||
lastY = y;
|
lastY = y;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const onPointerUp = () => {
|
const onPointerUp = withBatchedUpdates(() => {
|
||||||
isDraggingScrollBar = false;
|
isDraggingScrollBar = false;
|
||||||
setCursorForShape(this.state.elementType);
|
setCursorForShape(this.state.elementType);
|
||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
window.removeEventListener("pointermove", onPointerMove);
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
window.removeEventListener("pointerup", onPointerUp);
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
};
|
});
|
||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
@ -1624,7 +1648,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@ -1997,9 +2021,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const onPointerUp = (event: PointerEvent) => {
|
const onPointerUp = withBatchedUpdates((event: PointerEvent) => {
|
||||||
const {
|
const {
|
||||||
draggingElement,
|
draggingElement,
|
||||||
resizingElement,
|
resizingElement,
|
||||||
@ -2150,7 +2174,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
lastPointerUp = onPointerUp;
|
lastPointerUp = onPointerUp;
|
||||||
|
|
||||||
@ -2158,7 +2182,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.addEventListener("pointerup", onPointerUp);
|
window.addEventListener("pointerup", onPointerUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleWheel = (event: WheelEvent) => {
|
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { deltaX, deltaY } = event;
|
const { deltaX, deltaY } = event;
|
||||||
|
|
||||||
@ -2181,9 +2205,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
scrollX: normalizeScroll(scrollX - deltaX / zoom),
|
scrollX: normalizeScroll(scrollX - deltaX / zoom),
|
||||||
scrollY: normalizeScroll(scrollY - deltaY / zoom),
|
scrollY: normalizeScroll(scrollY - deltaY / zoom),
|
||||||
}));
|
}));
|
||||||
};
|
});
|
||||||
|
|
||||||
private beforeUnload = (event: BeforeUnloadEvent) => {
|
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||||
if (
|
if (
|
||||||
this.state.isCollaborating &&
|
this.state.isCollaborating &&
|
||||||
hasNonDeletedElements(globalSceneState.getAllElements())
|
hasNonDeletedElements(globalSceneState.getAllElements())
|
||||||
@ -2192,7 +2216,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
// NOTE: modern browsers no longer allow showing a custom message here
|
// NOTE: modern browsers no longer allow showing a custom message here
|
||||||
event.returnValue = "";
|
event.returnValue = "";
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
private addElementsFromPaste = (
|
private addElementsFromPaste = (
|
||||||
clipboardElements: readonly ExcalidrawElement[],
|
clipboardElements: readonly ExcalidrawElement[],
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
import React, { useLayoutEffect, useRef, useEffect } from "react";
|
||||||
import "./Popover.css";
|
import "./Popover.css";
|
||||||
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
top?: number;
|
top?: number;
|
||||||
@ -39,7 +40,7 @@ export function Popover({
|
|||||||
if (onCloseRequest) {
|
if (onCloseRequest) {
|
||||||
const handler = (e: Event) => {
|
const handler = (e: Event) => {
|
||||||
if (!popoverRef.current?.contains(e.target as Node)) {
|
if (!popoverRef.current?.contains(e.target as Node)) {
|
||||||
onCloseRequest();
|
unstable_batchedUpdates(() => onCloseRequest());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("pointerdown", handler, false);
|
document.addEventListener("pointerdown", handler, false);
|
||||||
|
@ -10,7 +10,8 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
|||||||
|
|
||||||
// This function tracks updates of text elements for the purposes for collaboration.
|
// This function tracks updates of text elements for the purposes for collaboration.
|
||||||
// The version is used to compare updates when more than one user is working in
|
// The version is used to compare updates when more than one user is working in
|
||||||
// the same drawing.
|
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||||
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
export function mutateElement<TElement extends ExcalidrawElement>(
|
export function mutateElement<TElement extends ExcalidrawElement>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user