mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
perf(editor): fallback to placeholder for canvas text (#12033)
### TL;DR For canvas elements, this PR adds placeholders during zooming operations to improve performance.  ### 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.  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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user