b5aff00c89
This PR does a few things to help with performance:
1. Instead of doing changes on raf we now do them 60 times per second.
This limits the number of updates on high refresh rate screens like the
iPad. With the current code this only applied to the history updates (so
when you subscribed to the updates), but the next point takes this a bit
futher.
2. We now trigger react updates 60 times per second. This is a change in
`useValue` and `useStateTracking` hooks.
3. We now throttle the inputs (like the `pointerMove`) in state nodes.
This means we batch multiple inputs and only apply them at most 60 times
per second.
We had to adjust our own tests to pass after this change so I marked
this as major as it might require the users of the library to do the
same.
Few observations:
- The browser calls the raf callbacks when it can. If it gets
overwhelmed it will call them further and further apart. As things call
down it will start calling them more frequently again. You can clearly
see this in the drawing example. When fps gets to a certain level we
start to get fewer updates, then fps can recover a bit. This makes the
experience quite janky. The updates can be kinda ok one second (dropping
frames, but consistently) and then they can completely stop and you have
to let go of the mouse to make them happen again. With the new logic it
seems everything is a lot more consistent.
- We might look into variable refresh rates to prevent this overtaxing
of the browser. Like when we see that the times between our updates are
getting higher we could make the updates less frequent. If we then see
that they are happening more often we could ramp them back up. I had an
[experiment for this
here](4834863966 (diff-318e71563d7c47173f89ec084ca44417cf70fc72faac85b96f48b856a8aec466L30-L35)
).
Few tests below. Used 6x slowdown for these.
# Resizing
### Before
https://github.com/tldraw/tldraw/assets/2523721/798a033f-5dfa-419e-9a2d-fd8908272ba0
### After
https://github.com/tldraw/tldraw/assets/2523721/45870a0c-c310-4be0-b63c-6c92c20ca037
# Drawing
Comparison is not 100% fair, we don't store the intermediate inputs
right now. That said, tick should still only produce once update so I do
think we can get a sense of the differences.
### Before
https://github.com/tldraw/tldraw/assets/2523721/2e8ac8c5-bbdf-484b-bb0c-70c967f4541c
### After
https://github.com/tldraw/tldraw/assets/2523721/8f54b7a8-9a0e-4a39-b168-482caceb0149
### Change Type
- [ ] `patch` — Bug fix
- [ ] `minor` — New feature
- [x] `major` — Breaking change
- [ ] `dependencies` — Changes to package dependencies[^1]
- [ ] `documentation` — Changes to the documentation only[^2]
- [ ] `tests` — Changes to any test code only[^2]
- [ ] `internal` — Any other changes that don't affect the published
package[^2]
- [ ] I don't know
[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version
### Release Notes
- Improves the performance of rendering.
---------
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
331 lines
9 KiB
Markdown
331 lines
9 KiB
Markdown
## API Report File for "@tldraw/utils"
|
|
|
|
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
|
|
|
```ts
|
|
|
|
// @internal
|
|
export function annotateError(error: unknown, annotations: Partial<ErrorAnnotations>): void;
|
|
|
|
// @internal (undocumented)
|
|
export function areArraysShallowEqual<T>(arr1: readonly T[], arr2: readonly T[]): boolean;
|
|
|
|
// @internal (undocumented)
|
|
export function areObjectsShallowEqual<T extends Record<string, unknown>>(obj1: T, obj2: T): boolean;
|
|
|
|
// @internal (undocumented)
|
|
export const assert: (value: unknown, message?: string) => asserts value;
|
|
|
|
// @internal (undocumented)
|
|
export const assertExists: <T>(value: T, message?: string | undefined) => NonNullable<T>;
|
|
|
|
// @internal
|
|
export function clearLocalStorage(): void;
|
|
|
|
// @internal
|
|
export function clearSessionStorage(): void;
|
|
|
|
// @internal (undocumented)
|
|
export function compact<T>(arr: T[]): NonNullable<T>[];
|
|
|
|
// @public
|
|
export function debounce<T extends unknown[], U>(callback: (...args: T) => PromiseLike<U> | U, wait: number): {
|
|
(...args: T): Promise<U>;
|
|
cancel(): void;
|
|
};
|
|
|
|
// @public
|
|
export function dedupe<T>(input: T[], equals?: (a: any, b: any) => boolean): T[];
|
|
|
|
// @public
|
|
export function deepCopy<T = unknown>(obj: T): T;
|
|
|
|
// @internal
|
|
export function deleteFromLocalStorage(key: string): void;
|
|
|
|
// @internal
|
|
export function deleteFromSessionStorage(key: string): void;
|
|
|
|
// @public (undocumented)
|
|
export type ErrorResult<E> = {
|
|
readonly ok: false;
|
|
readonly error: E;
|
|
};
|
|
|
|
// @internal (undocumented)
|
|
export function exhaustiveSwitchError(value: never, property?: string): never;
|
|
|
|
// @public (undocumented)
|
|
export type Expand<T> = T extends infer O ? {
|
|
[K in keyof O]: O[K];
|
|
} : never;
|
|
|
|
// @public
|
|
export class FileHelpers {
|
|
// @internal (undocumented)
|
|
static base64ToFile(dataURL: string): Promise<ArrayBuffer>;
|
|
static fileToBase64(file: Blob): Promise<string>;
|
|
}
|
|
|
|
// @internal
|
|
export function filterEntries<Key extends string, Value>(object: {
|
|
[K in Key]: Value;
|
|
}, predicate: (key: Key, value: Value) => boolean): {
|
|
[K in Key]: Value;
|
|
};
|
|
|
|
// @internal
|
|
export function fpsThrottle(fn: () => void): () => void;
|
|
|
|
// @internal (undocumented)
|
|
export function getErrorAnnotations(error: Error): ErrorAnnotations;
|
|
|
|
// @public
|
|
export function getFirstFromIterable<T = unknown>(set: Map<any, T> | Set<T>): T;
|
|
|
|
// @internal
|
|
export function getFromLocalStorage(key: string): null | string;
|
|
|
|
// @internal
|
|
export function getFromSessionStorage(key: string): null | string;
|
|
|
|
// @public
|
|
export function getHashForBuffer(buffer: ArrayBuffer): string;
|
|
|
|
// @public
|
|
export function getHashForObject(obj: any): string;
|
|
|
|
// @public
|
|
export function getHashForString(string: string): string;
|
|
|
|
// @public
|
|
export function getIndexAbove(below: IndexKey): IndexKey;
|
|
|
|
// @public
|
|
export function getIndexBelow(above: IndexKey): IndexKey;
|
|
|
|
// @public
|
|
export function getIndexBetween(below: IndexKey, above?: IndexKey): IndexKey;
|
|
|
|
// @public
|
|
export function getIndices(n: number, start?: IndexKey): IndexKey[];
|
|
|
|
// @public
|
|
export function getIndicesAbove(below: IndexKey, n: number): IndexKey[];
|
|
|
|
// @public
|
|
export function getIndicesBelow(above: IndexKey, n: number): IndexKey[];
|
|
|
|
// @public
|
|
export function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[];
|
|
|
|
// @internal (undocumented)
|
|
export function getOwnProperty<K extends string, V>(obj: Partial<Record<K, V>>, key: K): undefined | V;
|
|
|
|
// @internal (undocumented)
|
|
export function getOwnProperty(obj: object, key: string): unknown;
|
|
|
|
// @internal (undocumented)
|
|
export function hasOwnProperty(obj: object, key: string): boolean;
|
|
|
|
// @public
|
|
export type IndexKey = string & {
|
|
__orderKey: true;
|
|
};
|
|
|
|
// @public
|
|
export function invLerp(a: number, b: number, t: number): number;
|
|
|
|
// @public
|
|
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T;
|
|
|
|
// @public
|
|
export function isNonNull<T>(value: T): value is typeof value extends null ? never : T;
|
|
|
|
// @public
|
|
export function isNonNullish<T>(value: T): value is typeof value extends undefined ? never : typeof value extends null ? never : T;
|
|
|
|
// @public (undocumented)
|
|
export type JsonArray = JsonValue[];
|
|
|
|
// @public (undocumented)
|
|
export type JsonObject = {
|
|
[key: string]: JsonValue | undefined;
|
|
};
|
|
|
|
// @public (undocumented)
|
|
export type JsonPrimitive = boolean | null | number | string;
|
|
|
|
// @public (undocumented)
|
|
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
|
|
|
// @internal (undocumented)
|
|
export function last<T>(arr: readonly T[]): T | undefined;
|
|
|
|
// @public
|
|
export function lerp(a: number, b: number, t: number): number;
|
|
|
|
// @public (undocumented)
|
|
export function lns(str: string): string;
|
|
|
|
// @internal
|
|
export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(object: {
|
|
readonly [K in Key]: ValueBefore;
|
|
}, mapper: (key: Key, value: ValueBefore) => ValueAfter): {
|
|
[K in Key]: ValueAfter;
|
|
};
|
|
|
|
// @public
|
|
export class MediaHelpers {
|
|
static blobToDataUrl(blob: Blob): Promise<string>;
|
|
static getImageSize(blob: Blob): Promise<{
|
|
w: number;
|
|
h: number;
|
|
}>;
|
|
static getVideoSize(blob: Blob): Promise<{
|
|
w: number;
|
|
h: number;
|
|
}>;
|
|
static loadImage(src: string): Promise<HTMLImageElement>;
|
|
static loadVideo(src: string): Promise<HTMLVideoElement>;
|
|
// (undocumented)
|
|
static usingObjectURL<T>(blob: Blob, fn: (url: string) => Promise<T>): Promise<T>;
|
|
}
|
|
|
|
// @internal (undocumented)
|
|
export function minBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefined;
|
|
|
|
// @public
|
|
export function modulate(value: number, rangeA: number[], rangeB: number[], clamp?: boolean): number;
|
|
|
|
// @internal
|
|
export function noop(): void;
|
|
|
|
// @internal
|
|
export function objectMapEntries<Key extends string, Value>(object: {
|
|
[K in Key]: Value;
|
|
}): Array<[Key, Value]>;
|
|
|
|
// @internal
|
|
export function objectMapFromEntries<Key extends string, Value>(entries: ReadonlyArray<readonly [Key, Value]>): {
|
|
[K in Key]: Value;
|
|
};
|
|
|
|
// @internal
|
|
export function objectMapKeys<Key extends string>(object: {
|
|
readonly [K in Key]: unknown;
|
|
}): Array<Key>;
|
|
|
|
// @internal
|
|
export function objectMapValues<Key extends string, Value>(object: {
|
|
[K in Key]: Value;
|
|
}): Array<Value>;
|
|
|
|
// @public (undocumented)
|
|
export type OkResult<T> = {
|
|
readonly ok: true;
|
|
readonly value: T;
|
|
};
|
|
|
|
// @internal
|
|
export function omitFromStackTrace<Args extends Array<unknown>, Return>(fn: (...args: Args) => Return): (...args: Args) => Return;
|
|
|
|
// @internal
|
|
export function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]];
|
|
|
|
// @public (undocumented)
|
|
export class PngHelpers {
|
|
// (undocumented)
|
|
static findChunk(view: DataView, type: string): {
|
|
dataOffset: number;
|
|
size: number;
|
|
start: number;
|
|
};
|
|
// (undocumented)
|
|
static getChunkType(view: DataView, offset: number): string;
|
|
// (undocumented)
|
|
static isPng(view: DataView, offset: number): boolean;
|
|
// (undocumented)
|
|
static parsePhys(view: DataView, offset: number): {
|
|
ppux: number;
|
|
ppuy: number;
|
|
unit: number;
|
|
};
|
|
// (undocumented)
|
|
static readChunks(view: DataView, offset?: number): Record<string, {
|
|
dataOffset: number;
|
|
size: number;
|
|
start: number;
|
|
}>;
|
|
// (undocumented)
|
|
static setPhysChunk(view: DataView, dpr?: number, options?: BlobPropertyBag): Blob;
|
|
}
|
|
|
|
// @internal (undocumented)
|
|
export function promiseWithResolve<T>(): Promise<T> & {
|
|
resolve: (value: T) => void;
|
|
reject: (reason?: any) => void;
|
|
};
|
|
|
|
// @public (undocumented)
|
|
export type RecursivePartial<T> = {
|
|
[P in keyof T]?: RecursivePartial<T[P]>;
|
|
};
|
|
|
|
// @internal (undocumented)
|
|
type Required_2<T, K extends keyof T> = Expand<Omit<T, K> & _Required<Pick<T, K>>>;
|
|
export { Required_2 as Required }
|
|
|
|
// @public (undocumented)
|
|
export type Result<T, E> = ErrorResult<E> | OkResult<T>;
|
|
|
|
// @public (undocumented)
|
|
export const Result: {
|
|
ok<T>(value: T): OkResult<T>;
|
|
err<E>(error: E): ErrorResult<E>;
|
|
};
|
|
|
|
// @public
|
|
export function rng(seed?: string): () => number;
|
|
|
|
// @public
|
|
export function rotateArray<T>(arr: T[], offset: number): T[];
|
|
|
|
// @internal
|
|
export function setInLocalStorage(key: string, value: string): void;
|
|
|
|
// @internal
|
|
export function setInSessionStorage(key: string, value: string): void;
|
|
|
|
// @public (undocumented)
|
|
export function sortById<T extends {
|
|
id: any;
|
|
}>(a: T, b: T): -1 | 1;
|
|
|
|
// @public
|
|
export function sortByIndex<T extends {
|
|
index: IndexKey;
|
|
}>(a: T, b: T): -1 | 0 | 1;
|
|
|
|
// @public
|
|
const structuredClone_2: <T>(i: T) => T;
|
|
export { structuredClone_2 as structuredClone }
|
|
|
|
// @public
|
|
export function throttle<T extends (...args: any) => any>(func: T, limit: number): (...args: Parameters<T>) => ReturnType<T>;
|
|
|
|
// @internal
|
|
export function throttleToNextFrame(fn: () => void): void;
|
|
|
|
// @internal (undocumented)
|
|
export function validateIndexKey(key: string): asserts key is IndexKey;
|
|
|
|
// @internal (undocumented)
|
|
export function warnDeprecatedGetter(name: string): void;
|
|
|
|
// @public
|
|
export const ZERO_INDEX_KEY: IndexKey;
|
|
|
|
// (No @packageDocumentation comment for this package)
|
|
|
|
```
|