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,6 +1,7 @@
import type { SurfaceBlockModel } from '@blocksuite/affine/blocks/surface';
import type {
BrushElementModel,
ConnectorElementModel,
GroupElementModel,
} from '@blocksuite/affine/model';
import { beforeEach, describe, expect, test } from 'vitest';
@@ -160,6 +161,7 @@ describe('connector', () => {
expect(model.getConnectors(id).map(el => el.id)).toEqual([connector.id]);
expect(model.getConnectors(id2).map(el => el.id)).toEqual([connector.id]);
expect((connector as ConnectorElementModel).forceFullRender).toBe(true);
});
test('multiple connectors are supported', () => {
@@ -194,6 +196,8 @@ describe('connector', () => {
expect(model.getConnectors(id).map(c => c.id)).toEqual(connectors);
expect(model.getConnectors(id2).map(c => c.id)).toEqual(connectors);
expect((connector as ConnectorElementModel).forceFullRender).toBe(true);
expect((connector2 as ConnectorElementModel).forceFullRender).toBe(true);
});
test('should return null if connector are updated', () => {
@@ -213,6 +217,11 @@ describe('connector', () => {
},
});
const connectorBeforeUpdate = model.getElementById(connectorId)!;
expect(
(connectorBeforeUpdate as ConnectorElementModel).forceFullRender
).toBe(true);
model.updateElement(connectorId, {
source: {
position: [0, 0],
@@ -243,6 +252,11 @@ describe('connector', () => {
},
});
const connectorBeforeDelete = model.getElementById(connectorId)!;
expect(
(connectorBeforeDelete as ConnectorElementModel).forceFullRender
).toBe(true);
model.deleteElement(connectorId);
await wait();