From e3333a01bc341327aca17923fc63569411b70e74 Mon Sep 17 00:00:00 2001 From: Chen <99816898+donteatfriedrice@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:44:05 +0800 Subject: [PATCH] perf: optimize frame preview performance (#6169) --- .../components/frame/frame-preview.ts | 115 ++++++++++-------- .../blocks/src/surface-block/surface-model.ts | 12 +- .../frame-panel/body/frame-panel-body.ts | 2 +- .../fragments/frame-panel/card/frame-card.ts | 1 + 4 files changed, 75 insertions(+), 55 deletions(-) diff --git a/packages/blocks/src/page-block/edgeless/components/frame/frame-preview.ts b/packages/blocks/src/page-block/edgeless/components/frame/frame-preview.ts index 967b83629d70..ea4211670a3c 100644 --- a/packages/blocks/src/page-block/edgeless/components/frame/frame-preview.ts +++ b/packages/blocks/src/page-block/edgeless/components/frame/frame-preview.ts @@ -1,6 +1,6 @@ import '../../../../surface-ref-block/surface-ref-portal.js'; -import { DisposableGroup } from '@blocksuite/global/utils'; +import { debounce, DisposableGroup } from '@blocksuite/global/utils'; import { type EditorHost, ShadowlessElement, @@ -11,20 +11,21 @@ import { css, html, nothing, type PropertyValues } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; -import type { - EdgelessModel, - TopLevelBlockModel, -} from '../../../../_common/types.js'; -import { buildPath } from '../../../../_common/utils/index.js'; +import type { EdgelessModel } from '../../../../_common/types.js'; import type { FrameBlockModel } from '../../../../frame-block/frame-model.js'; import type { NoteBlockModel } from '../../../../note-block/note-model.js'; -import type { SurfaceBlockModel } from '../../../../surface-block/surface-model.js'; +import type { + ElementUpdatedData, + SurfaceBlockModel, +} from '../../../../surface-block/surface-model.js'; +import type { SurfaceService } from '../../../../surface-block/surface-service.js'; import { Bound } from '../../../../surface-block/utils/bound.js'; import { deserializeXYWH } from '../../../../surface-block/utils/xywh.js'; import type { SurfaceRefPortal } from '../../../../surface-ref-block/surface-ref-portal.js'; import type { SurfaceRefRenderer } from '../../../../surface-ref-block/surface-ref-renderer.js'; import type { SurfaceRefBlockService } from '../../../../surface-ref-block/surface-ref-service.js'; import type { EdgelessPageBlockComponent } from '../../edgeless-page-block.js'; +import { isTopLevelBlock } from '../../utils/query.js'; type RefElement = Exclude; @@ -76,6 +77,9 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) frame!: FrameBlockModel; + @property({ attribute: false }) + page!: Page; + @property({ attribute: false }) host!: EditorHost; @@ -104,10 +108,6 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { @query('.frame-preview-surface-container surface-ref-portal') blocksPortal!: SurfaceRefPortal; - get page() { - return this.host.page; - } - get surfaceRenderer() { return this._surfaceRefRenderer.surfaceRenderer; } @@ -136,6 +136,10 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { } } + private get _surfaceService() { + return this.host?.std.spec.getService('affine:surface') as SurfaceService; + } + private get _surfaceRefService() { const service = this.host.spec.getService('affine:surface-ref') as | SurfaceRefBlockService @@ -185,7 +189,7 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { } private _refreshViewport() { - if (!this.frame) { + if (!this.frame || !this._surfaceService) { return; } @@ -228,17 +232,36 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { }; }; - private _updateOnElementChange = (element: string | { id: string }) => { - const id = typeof element === 'string' ? element : element.id; + private _overlapWithFrame = (id: string) => { const ele = this.edgeless?.service.getElementById(id); - if (!ele || !ele.xywh) return; + if (!ele || !ele.xywh) return false; + const frameBound = Bound.deserialize(this.frame.xywh); const eleBound = Bound.deserialize(ele.xywh); - if (!frameBound.isOverlapWithBound(eleBound)) return; + return frameBound.isOverlapWithBound(eleBound); + }; - this._refreshViewport(); + private _handleElementUpdated = (data: ElementUpdatedData) => { + const { id, oldValues, props } = data; + if (!props.xywh) return; + // if element is moved in frame, refresh viewport + if (this._overlapWithFrame(id)) { + this._refreshViewport(); + } else if (oldValues.xywh) { + // if element is moved out of frame, refresh viewport + const oldBound = Bound.deserialize(oldValues.xywh as string); + const frameBound = Bound.deserialize(this.frame.xywh); + if (oldBound.isOverlapWithBound(frameBound)) { + this._refreshViewport(); + } + } }; + private _debounceHandleElementUpdated = debounce( + this._handleElementUpdated, + 1000 / 30 + ); + private _clearEdgelessDisposables = () => { this._edgelessDisposables?.dispose(); this._edgelessDisposables = null; @@ -267,15 +290,19 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { }) ); this._edgelessDisposables.add( - edgeless.service.surface.elementAdded.on(this._updateOnElementChange) + edgeless.service.surface.elementAdded.on(({ id }) => { + if (this._overlapWithFrame(id)) { + this._refreshViewport(); + } + }) ); this._edgelessDisposables.add( - edgeless.service.surface.elementUpdated.on(this._updateOnElementChange) + edgeless.service.surface.elementUpdated.on( + this._debounceHandleElementUpdated + ) ); this._edgelessDisposables.add( - edgeless.service.surface.elementRemoved.on(() => { - this._refreshViewport(); - }) + edgeless.service.surface.elementRemoved.on(() => this._refreshViewport()) ); } @@ -285,32 +312,18 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { this._pageDisposables.add( page.slots.blockUpdated.on(event => { - const { type, flavour } = event; - const isTopLevelBlock = ['affine:image', 'affine:note'].includes( - flavour - ); - if (!isTopLevelBlock) return; + const { type } = event; + // Should only check for add and delete events, the update event will be handled by the surface renderer + if (type === 'update') return; + + const model = + type === 'delete' ? event.model : page.getBlockById(event.id); + if (!model || !isTopLevelBlock(model) || !model.xywh) return; const frameBound = Bound.deserialize(this.frame.xywh); - if (type === 'delete') { - const deleteModel = event.model as TopLevelBlockModel; - const deleteBound = Bound.deserialize(deleteModel.xywh); - if (frameBound.containsPoint([deleteBound.x, deleteBound.y])) { - this._refreshViewport(); - } - } else { - const topLevelModel = page.getBlockById(event.id); - const topLevelBlock = this.host.view.viewFromPath( - 'block', - buildPath(topLevelModel) - ); - if (!topLevelBlock) return; - const newBound = Bound.deserialize( - (topLevelModel as TopLevelBlockModel).xywh - ); - if (frameBound.containsPoint([newBound.x, newBound.y])) { - this._refreshViewport(); - } + const modelBound = Bound.deserialize(model.xywh); + if (frameBound.containsPoint([modelBound.x, modelBound.y])) { + this._refreshViewport(); } }) ); @@ -386,9 +399,12 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { } else { this._clearEdgelessDisposables(); } + setTimeout(() => { + this._refreshViewport(); + }); } - if (_changedProperties.has('host')) { + if (_changedProperties.has('page')) { if (this.page) { this._setPageDisposables(this.page); } @@ -406,8 +422,9 @@ export class FramePreview extends WithDisposable(ShadowlessElement) { } override render() { - const { _surfaceModel, frame, host } = this; - const noContent = !_surfaceModel || !frame || !frame.xywh || !host; + const { _surfaceModel, frame, host, _surfaceService } = this; + const noContent = + !_surfaceModel || !frame || !frame.xywh || !host || !_surfaceService; return html`
${noContent ? nothing : this._renderSurfaceContent(frame)} diff --git a/packages/blocks/src/surface-block/surface-model.ts b/packages/blocks/src/surface-block/surface-model.ts index 5ae18dea958d..95e60b35bf63 100644 --- a/packages/blocks/src/surface-block/surface-model.ts +++ b/packages/blocks/src/surface-block/surface-model.ts @@ -29,6 +29,12 @@ export type SurfaceBlockProps = { elements: Boxed>>; }; +export interface ElementUpdatedData { + id: string; + props: Record; + oldValues: Record; +} + const migration = { toV4: data => { const { elements } = data; @@ -167,11 +173,7 @@ export class SurfaceBlockModel extends BlockModel { private _connectorToElements: Map = new Map(); private _elementToConnector: Map = new Map(); - elementUpdated = new Slot<{ - id: string; - props: Record; - oldValues: Record; - }>(); + elementUpdated = new Slot(); elementAdded = new Slot<{ id: string }>(); elementRemoved = new Slot<{ id: string; diff --git a/packages/presets/src/fragments/frame-panel/body/frame-panel-body.ts b/packages/presets/src/fragments/frame-panel/body/frame-panel-body.ts index ffc45c483023..33faf7a47e07 100644 --- a/packages/presets/src/fragments/frame-panel/body/frame-panel-body.ts +++ b/packages/presets/src/fragments/frame-panel/body/frame-panel-body.ts @@ -368,7 +368,7 @@ export class FramePanelBody extends WithDisposable(ShadowlessElement) { const selectedFrames = new Set(this._selected); const frameCards = html`${repeat( this._frameItems, - frameItem => frameItem.frame.id, + frameItem => [frameItem.frame.id, frameItem.cardIndex].join('-'), frameItem => { const { frame, frameIndex, cardIndex } = frameItem; return html``}