From b0cb29c11c52f13f9e0fe21654d7e5998d18e97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 11 Feb 2026 10:08:59 +0100 Subject: [PATCH] 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 --- .../utils/transform-rich-text-v2.util.ts | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts index 7d8bc4665d..02c5da32af 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util.ts @@ -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 => { - 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,