fix: disable render throttling by default & during resize (#5451)

This commit is contained in:
David Luzar 2022-07-16 11:36:55 +02:00 committed by GitHub
parent df14c69977
commit 5bc40402a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 66 additions and 14 deletions

View File

@ -166,7 +166,7 @@ import {
isAndroid, isAndroid,
} from "../keys"; } from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math"; import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderSceneThrottled } from "../renderer/renderScene"; import { renderScene } from "../renderer/renderScene";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { import {
calculateScrollCenter, calculateScrollCenter,
@ -286,6 +286,10 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
let touchTimeout = 0; let touchTimeout = 0;
let invalidateContextMenu = false; let invalidateContextMenu = false;
// remove this hack when we can sync render & resizeObserver (state update)
// to rAF. See #5439
let THROTTLE_NEXT_RENDER = true;
let lastPointerUp: ((event: any) => void) | null = null; let lastPointerUp: ((event: any) => void) | null = null;
const gesture: Gesture = { const gesture: Gesture = {
pointers: new Map(), pointers: new Map(),
@ -858,6 +862,7 @@ class App extends React.Component<AppProps, AppState> {
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) { if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
THROTTLE_NEXT_RENDER = false;
// recompute device dimensions state // recompute device dimensions state
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
this.refreshDeviceState(this.excalidrawContainerRef.current!); this.refreshDeviceState(this.excalidrawContainerRef.current!);
@ -1221,7 +1226,7 @@ class App extends React.Component<AppProps, AppState> {
); );
}); });
renderSceneThrottled( renderScene(
renderingElements, renderingElements,
this.state, this.state,
this.state.selectionElement, this.state.selectionElement,
@ -1259,8 +1264,13 @@ class App extends React.Component<AppProps, AppState> {
this.scheduleImageRefresh(); this.scheduleImageRefresh();
}, },
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
); );
if (!THROTTLE_NEXT_RENDER) {
THROTTLE_NEXT_RENDER = true;
}
this.history.record(this.state, this.scene.getElementsIncludingDeleted()); this.history.record(this.state, this.scene.getElementsIncludingDeleted());
// Do not notify consumers if we're still loading the scene. Among other // Do not notify consumers if we're still loading the scene. Among other

View File

@ -83,6 +83,8 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation"; import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
window.EXCALIDRAW_THROTTLE_RENDER = true;
const isExcalidrawPlusSignedUser = document.cookie.includes( const isExcalidrawPlusSignedUser = document.cookie.includes(
COOKIES.AUTH_STATE_COOKIE, COOKIES.AUTH_STATE_COOKIE,
); );

1
src/global.d.ts vendored
View File

@ -14,6 +14,7 @@ interface Window {
__EXCALIDRAW_SHA__: string | undefined; __EXCALIDRAW_SHA__: string | undefined;
EXCALIDRAW_ASSET_PATH: string | undefined; EXCALIDRAW_ASSET_PATH: string | undefined;
EXCALIDRAW_EXPORT_SOURCE: string; EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function; gtag: Function;
} }

View File

@ -181,7 +181,7 @@ const renderLinearPointHandles = (
context.restore(); context.restore();
}; };
export const renderScene = ( export const _renderScene = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
selectionElement: NonDeletedExcalidrawElement | null, selectionElement: NonDeletedExcalidrawElement | null,
@ -572,8 +572,7 @@ export const renderScene = (
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
}; };
/** renderScene throttled to animation framerate */ const renderSceneThrottled = throttleRAF(
export const renderSceneThrottled = throttleRAF(
( (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
@ -582,9 +581,9 @@ export const renderSceneThrottled = throttleRAF(
rc: RoughCanvas, rc: RoughCanvas,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
renderConfig: RenderConfig, renderConfig: RenderConfig,
callback?: (data: ReturnType<typeof renderScene>) => void, callback?: (data: ReturnType<typeof _renderScene>) => void,
) => { ) => {
const ret = renderScene( const ret = _renderScene(
elements, elements,
appState, appState,
selectionElement, selectionElement,
@ -598,6 +597,46 @@ export const renderSceneThrottled = throttleRAF(
{ trailing: true }, { trailing: true },
); );
/** renderScene throttled to animation framerate */
export const renderScene = <T extends boolean = false>(
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
selectionElement: NonDeletedExcalidrawElement | null,
scale: number,
rc: RoughCanvas,
canvas: HTMLCanvasElement,
renderConfig: RenderConfig,
callback?: (data: ReturnType<typeof _renderScene>) => void,
/** Whether to throttle rendering. Defaults to false.
* When throttling, no value is returned. Use the callback instead. */
throttle?: T,
): T extends true ? void : ReturnType<typeof _renderScene> => {
if (throttle) {
renderSceneThrottled(
elements,
appState,
selectionElement,
scale,
rc,
canvas,
renderConfig,
callback,
);
return undefined as T extends true ? void : ReturnType<typeof _renderScene>;
}
const ret = _renderScene(
elements,
appState,
selectionElement,
scale,
rc,
canvas,
renderConfig,
);
callback?.(ret);
return ret as T extends true ? void : ReturnType<typeof _renderScene>;
};
const renderTransformHandles = ( const renderTransformHandles = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderConfig: RenderConfig, renderConfig: RenderConfig,

View File

@ -39,7 +39,7 @@ const mouse = new Pointer("mouse");
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();

View File

@ -14,7 +14,7 @@ import { reseed } from "../random";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();

View File

@ -16,7 +16,7 @@ import { KEYS } from "../keys";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();

View File

@ -14,7 +14,7 @@ import { reseed } from "../random";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();

View File

@ -20,7 +20,7 @@ import { t } from "../i18n";
const { h } = window; const { h } = window;
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
const finger1 = new Pointer("touch", 1); const finger1 = new Pointer("touch", 1);

View File

@ -18,7 +18,7 @@ const mouse = new Pointer("mouse");
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();

View File

@ -16,7 +16,7 @@ import { Keyboard, Pointer } from "./helpers/ui";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderSceneThrottled"); const renderScene = jest.spyOn(Renderer, "renderScene");
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
renderScene.mockClear(); renderScene.mockClear();