Speed up compositing and gesture start without behavior changes
- Memoize each operation's choreography effect set in a computed, so nodes sharing an edge stop re-evaluating it (linear, not quadratic, in chain depth). - Cache the scene viewport and refresh it from a ResizeObserver and at each begin(), instead of measuring the container per node per frame. - Index operations by origin key so outgoing lookups no longer scan every operation; all map changes go through attach()/detach(). - Resolve px and % gesture edge distances arithmetically; only other CSS lengths use the off-screen probe. Verified in Chromium to match the probe within its 1/64px layout rounding. The differential suites against main still match exactly. Benchmarks vs main: styleForNode 2.0x (3 live ops) and 2.4x (10 live ops), rendered frames 1.14x. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
This commit is contained in:
@@ -76,6 +76,14 @@ export class SceneGraph {
|
||||
* that back operation's id. Cancelling the back operation restores the edge.
|
||||
*/
|
||||
private readonly suspendedEdges = new Map<number, MutableOriginOperation>();
|
||||
/**
|
||||
* Operations indexed by origin key, in insertion order. Every addition and
|
||||
* removal goes through {@link attach} and {@link detach} to keep it in sync
|
||||
* with `operations`.
|
||||
*/
|
||||
private readonly outgoing = shallowReactive(
|
||||
new Map<string, readonly MutableOriginOperation[]>(),
|
||||
);
|
||||
private nodeSequence = 0;
|
||||
private operationSequence = 0;
|
||||
|
||||
@@ -117,10 +125,9 @@ export class SceneGraph {
|
||||
|
||||
/** The unfinished operation originated by a node, if any. */
|
||||
liveOutgoingOf(nodeKey: string): MutableOriginOperation | undefined {
|
||||
for (const operation of this.operations.values())
|
||||
if (operation.originKey === nodeKey && operation.phase !== "finished")
|
||||
return operation;
|
||||
return undefined;
|
||||
return this.outgoing
|
||||
.get(nodeKey)
|
||||
?.find((operation) => operation.phase !== "finished");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,9 +135,7 @@ export class SceneGraph {
|
||||
* {@link liveOutgoingOf}, this includes a finished, connected resting edge.
|
||||
*/
|
||||
presentedOutgoingOf(nodeKey: string): MutableOriginOperation | undefined {
|
||||
for (const operation of this.operations.values())
|
||||
if (operation.originKey === nodeKey) return operation;
|
||||
return undefined;
|
||||
return this.outgoing.get(nodeKey)?.[0];
|
||||
}
|
||||
|
||||
/** Retained entries preceding `node`, oldest first. */
|
||||
@@ -257,7 +262,7 @@ export class SceneGraph {
|
||||
|
||||
this.transferPosition(origin, target);
|
||||
operation.phase = "finished";
|
||||
this.operations.delete(operation.id);
|
||||
this.detach(operation);
|
||||
this.suspendedEdges.delete(operation.id);
|
||||
|
||||
if (operation.history === "push") {
|
||||
@@ -286,7 +291,7 @@ export class SceneGraph {
|
||||
const target = this.nodes.get(operation.entryTargetKey);
|
||||
const suspendedEdge = this.suspendedEdges.get(id);
|
||||
operation.phase = "finished";
|
||||
this.operations.delete(id);
|
||||
this.detach(operation);
|
||||
this.suspendedEdges.delete(id);
|
||||
|
||||
if (operation.history === "back") {
|
||||
@@ -294,7 +299,7 @@ export class SceneGraph {
|
||||
// Put the retained page back into its former position, then reconnect
|
||||
// the resting page→drawer edge. Both nodes keep their VNodes.
|
||||
this.transferPosition(origin, target);
|
||||
this.operations.set(suspendedEdge.id, suspendedEdge);
|
||||
this.attach(suspendedEdge);
|
||||
origin.incomingOperationId = suspendedEdge.id;
|
||||
target.state = "exposed";
|
||||
} else if (target) {
|
||||
@@ -362,7 +367,7 @@ export class SceneGraph {
|
||||
const resting = this.operations.get(incoming.id);
|
||||
if (resting?.phase === "finished" && resting.choreography.persistAtRest) {
|
||||
// Re-root A→page→drawer as A→drawer before adding drawer→page.
|
||||
this.operations.delete(resting.id);
|
||||
this.detach(resting);
|
||||
this.transferPosition(target, origin);
|
||||
target.incomingOperationId = undefined;
|
||||
suspendedEdge = resting;
|
||||
@@ -461,10 +466,25 @@ export class SceneGraph {
|
||||
intent: "undecided",
|
||||
originRect: init.originRect,
|
||||
});
|
||||
this.operations.set(operation.id, operation);
|
||||
this.attach(operation);
|
||||
return operation;
|
||||
}
|
||||
|
||||
private attach(operation: MutableOriginOperation): void {
|
||||
this.operations.set(operation.id, operation);
|
||||
const siblings = this.outgoing.get(operation.originKey) ?? [];
|
||||
this.outgoing.set(operation.originKey, [...siblings, operation]);
|
||||
}
|
||||
|
||||
private detach(operation: MutableOriginOperation): void {
|
||||
this.operations.delete(operation.id);
|
||||
const siblings = (this.outgoing.get(operation.originKey) ?? []).filter(
|
||||
(sibling) => sibling.id !== operation.id,
|
||||
);
|
||||
if (siblings.length) this.outgoing.set(operation.originKey, siblings);
|
||||
else this.outgoing.delete(operation.originKey);
|
||||
}
|
||||
|
||||
private removeNode(nodeKey: string): void {
|
||||
this.nodes.delete(nodeKey);
|
||||
this.roots.value = this.roots.value.filter((key) => key !== nodeKey);
|
||||
@@ -487,7 +507,7 @@ export class SceneGraph {
|
||||
operation.targetKey === nodeKey ||
|
||||
operation.entryTargetKey === nodeKey
|
||||
)
|
||||
this.operations.delete(operation.id);
|
||||
this.detach(operation);
|
||||
this.removeNode(nodeKey);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user