perf: cache ServerBlockNoteEditor instance in transformRichTextV2Value (#17844)

## Summary
- `transformRichTextV2Value` was calling `await
import('@blocknote/server-util')` and `ServerBlockNoteEditor.create()`
on **every single invocation**, adding ~90ms of overhead each time
(visible as the highest avg-duration frame in profiling at 93.49ms).
- Cache the `ServerBlockNoteEditor` instance at module level so the
dynamic import + creation only happens once for the lifetime of the
process.
- Also removes debug timing instrumentation (`performance.now()`,
`calculateInputSize`, `Logger`) that is no longer needed.

## Test plan
- [ ] Verify rich text fields (blocknote/markdown) still round-trip
correctly on create and update
- [ ] Confirm reduced CPU time for `transformRichTextV2Value` in
profiling


Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Félix Malfait
2026-02-11 10:08:59 +01:00
committed by GitHub
parent b4d957306e
commit b0cb29c11c
@@ -1,5 +1,3 @@
import { Logger } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import {
type RichTextV2Metadata,
@@ -7,35 +5,30 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
const logger = new Logger('TransformRichTextV2');
// Reuse a single ServerBlockNoteEditor across all calls to avoid
// the cost of dynamic import resolution + instance creation (~90ms) on every transform.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let cachedServerBlockNoteEditor: any = null;
const calculateInputSize = (input: unknown): number => {
if (typeof input === 'string') {
return Buffer.byteLength(input, 'utf8');
}
if (typeof input === 'object' && input !== null) {
return Buffer.byteLength(JSON.stringify(input), 'utf8');
const getServerBlockNoteEditor = async () => {
if (!cachedServerBlockNoteEditor) {
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
cachedServerBlockNoteEditor = ServerBlockNoteEditor.create();
}
return 0;
return cachedServerBlockNoteEditor;
};
export const transformRichTextV2Value = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
richTextValue: any,
): Promise<RichTextV2Metadata> => {
const startTime = performance.now();
const inputSize = calculateInputSize(richTextValue);
const parsedValue = isNonEmptyString(richTextValue)
? richTextV2ValueSchema.parse(richTextValue)
: richTextValue;
const afterParsingTime = performance.now();
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
const serverBlockNoteEditor = await getServerBlockNoteEditor();
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)
// Todo : This may be resolved once the server-utils library is updated with proper conversion support - #947
@@ -51,8 +44,6 @@ export const transformRichTextV2Value = async (
convertedMarkdown = parsedValue.blocknote || null;
}
const afterMarkdownConversionTime = performance.now();
const convertedBlocknote = parsedValue.markdown
? JSON.stringify(
await serverBlockNoteEditor.tryParseMarkdownToBlocks(
@@ -61,12 +52,6 @@ export const transformRichTextV2Value = async (
)
: null;
const endTime = performance.now();
logger.debug(
`transformRichTextV2Value completed - total: ${endTime - startTime}ms, parsing: ${afterParsingTime - startTime}ms, markdown_conversion: ${afterMarkdownConversionTime - afterParsingTime}ms, blocknote_conversion: ${endTime - afterMarkdownConversionTime}ms, size: ${inputSize} bytes`,
);
return {
markdown: parsedValue.markdown || convertedMarkdown,
blocknote: parsedValue.blocknote || convertedBlocknote,