perf(editor): fallback to placeholder for canvas text (#12033)

### TL;DR

For canvas elements, this PR adds placeholders during zooming operations to improve performance.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/8c8daea8-1eb4-419b-a4f4-2a8847f40b7b.png)

### What changed?

- Implemented placeholder rendering during zooming operations in the canvas renderer, but not only DOM.
- Added a `forceFullRender` property to the `GfxCompatibleInterface` to allow elements to opt out of placeholder rendering
- Set `forceFullRender = true` for connectors to ensure they always render properly, even during zooming
- Connected the turbo renderer to the viewport's zooming state to automatically switch between full and placeholder rendering

### Why make this change?

Rendering complex elements during zooming operations can cause performance issues and make the UI feel sluggish. Rendering connector label also leads to high cost DOM `set font` delays.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/961fb847-24b4-4a7f-b9dc-21b0a5edaaa1.png)

The turbo renderer improves performance by displaying simple placeholders for elements during zooming, while still rendering critical elements like connectors fully. This creates a smoother user experience while maintaining essential visual information.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a feature-flag-controlled "turbo" rendering mode that displays placeholder graphics during zooming for improved performance.
  - Added the ability to override placeholder rendering for specific elements, ensuring full rendering when required.

- **Bug Fixes**
  - Enhanced rendering logic to ensure connectors always render fully, even during zoom operations.

- **Documentation**
  - Updated API documentation to reflect new properties related to rendering behavior.

- **Tests**
  - Improved tests to verify correct rendering behavior for connectors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
doodlewind
2025-04-29 03:05:17 +00:00
parent d82d37b53d
commit be28038e94
9 changed files with 107 additions and 18 deletions

View File

@@ -1,10 +1,12 @@
import { type Color, ColorScheme } from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IBound } from '@blocksuite/global/gfx';
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import type {
GfxCompatibleInterface,
GridManager,
LayerManager,
SurfaceBlockModel,
@@ -43,6 +45,8 @@ export class CanvasRenderer {
private readonly _disposables = new DisposableGroup();
private readonly _turboEnabled: () => boolean;
private readonly _overlays = new Set<Overlay>();
private _refreshRafId: number | null = null;
@@ -67,6 +71,8 @@ export class CanvasRenderer {
removed: HTMLCanvasElement[];
}>();
usePlaceholder = false;
viewport: Viewport;
get stackingCanvas() {
@@ -83,6 +89,12 @@ export class CanvasRenderer {
this.layerManager = options.layerManager;
this.grid = options.gridManager;
this.provider = options.provider ?? {};
this._turboEnabled = () => {
const featureFlagService = options.std.get(FeatureFlagService);
return featureFlagService.getFlag('enable_turbo_renderer');
};
this._initViewport();
options.enableStackingCanvas = options.enableStackingCanvas ?? false;
@@ -213,6 +225,19 @@ export class CanvasRenderer {
}, this._container);
})
);
this._disposables.add(
this.viewport.zooming$.subscribe(isZooming => {
const shouldRenderPlaceholders = this._turboEnabled() && isZooming;
if (this.usePlaceholder !== shouldRenderPlaceholders) {
this.usePlaceholder = shouldRenderPlaceholders;
this.refresh();
}
})
);
this.usePlaceholder = false;
}
private _render() {
@@ -279,23 +304,30 @@ export class CanvasRenderer {
for (const element of elements) {
const display = (element.display ?? true) && !element.hidden;
if (display && intersects(getBoundWithRotation(element), bound)) {
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
);
if (
this.usePlaceholder &&
!(element as GfxCompatibleInterface).forceFullRender
) {
ctx.save();
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
const drawX = element.x - bound.x;
const drawY = element.y - bound.y;
ctx.fillRect(drawX, drawY, element.w, element.h);
ctx.restore();
} else {
ctx.save();
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
);
if (!renderFn) {
console.warn(`Cannot find renderer for ${element.type}`);
continue;
if (!renderFn) continue;
ctx.globalAlpha = element.opacity ?? 1;
const dx = element.x - bound.x;
const dy = element.y - bound.y;
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
ctx.restore();
}
ctx.save();
ctx.globalAlpha = element.opacity ?? 1;
const dx = element.x - bound.x;
const dy = element.y - bound.y;
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
ctx.restore();
}
}

View File

@@ -249,13 +249,20 @@ function renderLabel(
const [, , w, h] = labelXYWH!;
const cx = w / 2;
const cy = h / 2;
ctx.setTransform(matrix);
if (renderer.usePlaceholder) {
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
ctx.fillRect(0, 0, w, h);
return; // Skip actual label rendering
}
const deltas = wrapTextDeltas(text!, font, w);
const lines = deltaInsertsToChunks(deltas);
const lineHeight = getLineHeight(fontFamily, fontSize, fontWeight);
const textHeight = (lines.length - 1) * lineHeight * 0.5;
ctx.setTransform(matrix);
ctx.font = font;
ctx.textAlign = textAlign;
ctx.textBaseline = 'middle';

View File

@@ -106,6 +106,11 @@ export type ConnectorElementProps = BaseElementProps & {
export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorElementProps> {
updatingPath = false;
/**
* Connectors should always render, even during zoom.
*/
forceFullRender = true;
override get connectable() {
return false as const;
}

View File

@@ -19,6 +19,7 @@ export interface BlockSuiteFlags {
enable_callout: boolean;
enable_edgeless_scribbled_style: boolean;
enable_embed_doc_with_alias: boolean;
enable_turbo_renderer: boolean;
}
export class FeatureFlagService extends StoreExtension {
@@ -42,6 +43,7 @@ export class FeatureFlagService extends StoreExtension {
enable_callout: false,
enable_edgeless_scribbled_style: false,
enable_embed_doc_with_alias: false,
enable_turbo_renderer: false,
});
setFlag(key: keyof BlockSuiteFlags, value: boolean) {