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:

![idle-issue.jpg](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/9c8c2150-69d4-416b-b46e-8473a7fdf339.jpg)

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:
doodlewind
2025-03-08 01:38:02 +00:00
parent dc047aa1a4
commit 334912e85b
12 changed files with 176 additions and 98 deletions

View File

@@ -10,6 +10,7 @@ import {
getImageSelectionsCommand,
getSelectedBlocksCommand,
getSelectedModelsCommand,
getSurfaceBlock,
getTextSelectionCommand,
ImageBlockModel,
isCanvasElement,
@@ -197,7 +198,7 @@ export const stopPropagation = (e: Event) => {
export function getSurfaceElementFromEditor(editor: EditorHost) {
const { doc } = editor;
const surfaceModel = doc.getBlockByFlavour('affine:surface')[0];
const surfaceModel = getSurfaceBlock(doc);
if (!surfaceModel) return null;
const surfaceId = surfaceModel.id;

View File

@@ -15,6 +15,7 @@ import {
FontFamilyMap,
FontStyle,
FontWeightMap,
getSurfaceBlock,
PointStyle,
StrokeStyle,
TextAlign,
@@ -29,7 +30,6 @@ import { menuTrigger, settingWrapper } from '../style.css';
import { sortedFontWeightEntries, usePalettes } from '../utils';
import { Point } from './point';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
enum ConnecterStyle {
General = 'general',

View File

@@ -7,7 +7,11 @@ import {
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { LayoutType, MindmapStyle } from '@blocksuite/affine/blocks';
import {
getSurfaceBlock,
LayoutType,
MindmapStyle,
} from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store';
import { useFramework, useLiveData } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
@@ -15,7 +19,6 @@ import { useCallback, useMemo } from 'react';
import { DropdownMenu } from '../menu';
import { menuTrigger, settingWrapper } from '../style.css';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
const MINDMAP_STYLES = [
{

View File

@@ -2,7 +2,7 @@ import { MenuItem, MenuTrigger, Slider } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { useI18n } from '@affine/i18n';
import { DefaultTheme } from '@blocksuite/affine/blocks';
import { DefaultTheme, getSurfaceBlock } from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store';
import { useFramework, useLiveData } from '@toeverything/infra';
import { isEqual } from 'lodash-es';
@@ -13,7 +13,6 @@ import { menuTrigger } from '../style.css';
import { usePalettes } from '../utils';
import { Point } from './point';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
export const PenSettings = () => {
const t = useI18n();

View File

@@ -18,6 +18,7 @@ import {
FontStyle,
FontWeightMap,
getShapeName,
getSurfaceBlock,
ShapeStyle,
ShapeType,
StrokeStyle,
@@ -39,7 +40,6 @@ import { sortedFontWeightEntries, usePalettes } from '../utils';
import type { DocName } from './docs';
import { Point } from './point';
import { EdgelessSnapshot } from './snapshot';
import { getSurfaceBlock } from './utils';
enum ShapeTextFontSize {
'16px' = '16',

View File

@@ -1,12 +1,6 @@
import type { SurfaceBlockModel } from '@blocksuite/affine/block-std/gfx';
import type { FrameBlockModel } from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store';
export function getSurfaceBlock(doc: Store) {
const blocks = doc.getBlocksByFlavour('affine:surface');
return blocks.length !== 0 ? (blocks[0].model as SurfaceBlockModel) : null;
}
export function getFrameBlock(doc: Store) {
const blocks = doc.getBlocksByFlavour('affine:frame');
return blocks.length !== 0 ? (blocks[0].model as FrameBlockModel) : null;