mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
perf(editor): lazy DOM update with idle state in gfx viewport (#10624)
Currently, `GfxViewportElement` hides DOM blocks outside the viewport using `display: none` to optimize performance. However, this approach presents two issues: 1. Even when hidden, all top-level blocks still undergo frequent CSS transform updates during viewport panning and zooming. 2. Hidden blocks cannot access DOM layout information, preventing `TurboRenderer` from updating the complete canvas bitmap. To address this, this PR introduces a refactoring that divides all top-level edgeless blocks into two states: `idle` and `active`. The improvements are as follows: 1. Blocks outside the viewport are set to the `idle` state, meaning they no longer update their DOM during viewport panning or zooming. Only `active` blocks within the viewport are updated frame by frame. 2. For `idle` blocks, the hiding method switches from `display: none` to `visibility: hidden`, ensuring their layout information remains accessible to `TurboRenderer`. [Screen Recording 2025-03-07 at 3.23.56 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/lEGcysB4lFTEbCwZ8jMv/4bac640b-f5b6-4b0b-904d-5899f96cf375.mov" />](https://app.graphite.dev/media/video/lEGcysB4lFTEbCwZ8jMv/4bac640b-f5b6-4b0b-904d-5899f96cf375.mov) While this minimizes DOM updates, it introduces a trade-off: `idle` blocks retain an outdated layout state. Since their positions are updated using a lazy update strategy, their layout state remains frozen at the moment they were last moved out of the viewport:  To resolve this, the PR serializes and stores the viewport field of the block at that moment on the `idle` block itself. This allows the correct layout, positioned in the model coordinate system, to be restored from the stored data.
This commit is contained in:
@@ -24,6 +24,29 @@ export const ZOOM_INITIAL = 1.0;
|
||||
|
||||
export const FIT_TO_SCREEN_PADDING = 100;
|
||||
|
||||
export interface ViewportRecord {
|
||||
left: number;
|
||||
top: number;
|
||||
viewportX: number;
|
||||
viewportY: number;
|
||||
zoom: number;
|
||||
viewScale: number;
|
||||
}
|
||||
|
||||
export function clientToModelCoord(
|
||||
viewport: ViewportRecord,
|
||||
clientCoord: [number, number]
|
||||
): IVec {
|
||||
const { left, top, viewportX, viewportY, zoom, viewScale } = viewport;
|
||||
|
||||
const [clientX, clientY] = clientCoord;
|
||||
const viewportInternalX = clientX - left;
|
||||
const viewportInternalY = clientY - top;
|
||||
const modelX = viewportX + viewportInternalX / zoom / viewScale;
|
||||
const modelY = viewportY + viewportInternalY / zoom / viewScale;
|
||||
return [modelX, modelY];
|
||||
}
|
||||
|
||||
export class Viewport {
|
||||
private _cachedBoundingClientRect: DOMRect | null = null;
|
||||
|
||||
@@ -461,8 +484,7 @@ export class Viewport {
|
||||
}
|
||||
|
||||
toModelCoordFromClientCoord([x, y]: IVec): IVec {
|
||||
const { left, top } = this;
|
||||
return this.toModelCoord(x - left, y - top);
|
||||
return clientToModelCoord(this, [x, y]);
|
||||
}
|
||||
|
||||
toViewBound(bound: Bound) {
|
||||
@@ -484,4 +506,26 @@ export class Viewport {
|
||||
const { left, top } = this;
|
||||
return [x - left, y - top];
|
||||
}
|
||||
|
||||
serializeRecord() {
|
||||
return JSON.stringify({
|
||||
left: this.left,
|
||||
top: this.top,
|
||||
viewportX: this.viewportX,
|
||||
viewportY: this.viewportY,
|
||||
zoom: this.zoom,
|
||||
viewScale: this.viewScale,
|
||||
});
|
||||
}
|
||||
|
||||
deserializeRecord(record?: string) {
|
||||
try {
|
||||
const result = JSON.parse(record || '{}') as ViewportRecord;
|
||||
if (!('zoom' in result)) return null;
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to deserialize viewport record:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user