Files
AFFiNE-Mirror/blocksuite/affine/shared/src/services/font-loader/font-loader-service.ts
DarkSky 72df9cb457 feat: improve editor performance (#14429)
#### PR Dependency Tree


* **PR #14429** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* HTML import now splits lines on <br> into separate paragraphs while
preserving inline formatting.

* **Bug Fixes**
* Paste falls back to inserting after the first paragraph when no
explicit target is found.

* **Style**
  * Improved page-mode viewport styling for consistent content layout.

* **Tests**
* Added snapshot tests for <br>-based paragraph splitting; re-enabled an
e2e drag-page test.

* **Chores**
* Deferred/deduplicated font loading, inline text caching,
drag-handle/pointer optimizations, and safer inline render
synchronization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-14 00:43:36 +08:00

202 lines
5.6 KiB
TypeScript

import { FontFamily, FontStyle, FontWeight } from '@blocksuite/affine-model';
import { createIdentifier } from '@blocksuite/global/di';
import { IS_FIREFOX } from '@blocksuite/global/env';
import { LifeCycleWatcher } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import type { FontConfig } from './config.js';
const initFontFace = IS_FIREFOX
? ({ font, weight, url, style }: FontConfig) =>
new FontFace(`"${font}"`, `url(${url})`, {
weight,
style,
})
: ({ font, weight, url, style }: FontConfig) =>
new FontFace(font, `url(${url})`, {
weight,
style,
});
export class FontLoaderService extends LifeCycleWatcher {
static override readonly key = 'font-loader';
private static readonly DEFERRED_LOAD_DELAY_MS = 5000;
private static readonly DEFERRED_LOAD_BATCH_SIZE = 4;
private static readonly DEFERRED_LOAD_BATCH_INTERVAL_MS = 1000;
private _idleLoadTaskId: number | null = null;
private _lazyLoadTimeoutId: number | null = null;
private _deferredFontsQueue: FontConfig[] = [];
private _deferredFontsCursor = 0;
private readonly _loadedFontKeys = new Set<string>();
readonly fontFaces: FontFace[] = [];
get ready() {
return Promise.all(this.fontFaces.map(fontFace => fontFace.loaded));
}
private readonly _fontKey = ({ font, weight, style, url }: FontConfig) => {
return `${font}:${weight}:${style}:${url}`;
};
private readonly _isCriticalCanvasFont = ({
font,
weight,
style,
}: FontConfig) => {
if (style !== FontStyle.Normal) return false;
if (font === FontFamily.Poppins) {
return (
weight === FontWeight.Regular ||
weight === FontWeight.Medium ||
weight === FontWeight.SemiBold
);
}
if (font === FontFamily.Inter) {
return weight === FontWeight.Regular || weight === FontWeight.SemiBold;
}
if (font === FontFamily.Kalam) {
// Mindmap style four uses bold Kalam text.
// We map to SemiBold because this is the strongest shipped Kalam weight.
return weight === FontWeight.SemiBold;
}
return false;
};
private readonly _scheduleDeferredLoad = (fonts: FontConfig[]) => {
if (fonts.length === 0 || typeof window === 'undefined') {
return;
}
this._deferredFontsQueue = fonts;
this._deferredFontsCursor = 0;
const win = window as Window & {
requestIdleCallback?: (
callback: () => void,
options?: { timeout?: number }
) => number;
cancelIdleCallback?: (handle: number) => void;
};
const scheduleBatch = (delayMs: number) => {
this._lazyLoadTimeoutId = window.setTimeout(() => {
this._lazyLoadTimeoutId = null;
const runBatch = () => {
this._idleLoadTaskId = null;
const start = this._deferredFontsCursor;
const end = Math.min(
start + FontLoaderService.DEFERRED_LOAD_BATCH_SIZE,
this._deferredFontsQueue.length
);
const batch = this._deferredFontsQueue.slice(start, end);
this._deferredFontsCursor = end;
this.load(batch);
if (this._deferredFontsCursor < this._deferredFontsQueue.length) {
scheduleBatch(FontLoaderService.DEFERRED_LOAD_BATCH_INTERVAL_MS);
}
};
if (typeof win.requestIdleCallback === 'function') {
this._idleLoadTaskId = win.requestIdleCallback(runBatch, {
timeout: 2000,
});
return;
}
runBatch();
}, delayMs);
};
scheduleBatch(FontLoaderService.DEFERRED_LOAD_DELAY_MS);
};
private readonly _cancelDeferredLoad = () => {
if (typeof window === 'undefined') {
return;
}
const win = window as Window & {
cancelIdleCallback?: (handle: number) => void;
};
if (
this._idleLoadTaskId !== null &&
typeof win.cancelIdleCallback === 'function'
) {
win.cancelIdleCallback(this._idleLoadTaskId);
this._idleLoadTaskId = null;
}
if (this._lazyLoadTimeoutId !== null) {
window.clearTimeout(this._lazyLoadTimeoutId);
this._lazyLoadTimeoutId = null;
}
this._deferredFontsQueue = [];
this._deferredFontsCursor = 0;
};
load(fonts: FontConfig[]) {
for (const font of fonts) {
const key = this._fontKey(font);
if (this._loadedFontKeys.has(key)) {
continue;
}
this._loadedFontKeys.add(key);
const fontFace = initFontFace(font);
document.fonts.add(fontFace);
fontFace.load().catch(console.error);
this.fontFaces.push(fontFace);
}
}
override mounted() {
const config = this.std.getOptional(FontConfigIdentifier);
if (!config || config.length === 0) {
return;
}
const criticalFonts = config.filter(this._isCriticalCanvasFont);
const eagerFonts =
criticalFonts.length > 0 ? criticalFonts : config.slice(0, 3);
const eagerFontKeySet = new Set(eagerFonts.map(this._fontKey));
const deferredFonts = config.filter(
font => !eagerFontKeySet.has(this._fontKey(font))
);
this.load(eagerFonts);
this._scheduleDeferredLoad(deferredFonts);
}
override unmounted() {
this._cancelDeferredLoad();
for (const fontFace of this.fontFaces) {
document.fonts.delete(fontFace);
}
this.fontFaces.splice(0, this.fontFaces.length);
this._loadedFontKeys.clear();
}
}
export const FontConfigIdentifier =
createIdentifier<FontConfig[]>('AffineFontConfig');
export const FontConfigExtension = (
fontConfig: FontConfig[]
): ExtensionType => ({
setup: di => {
di.addImpl(FontConfigIdentifier, () => fontConfig);
},
});