From d45d579ea42ee0549e4cc3905ee15faa3995b16a Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Tue, 27 Oct 2026 11:43:05 -0700 Subject: [PATCH] perf(notebook): skip re-rendering unchanged cell outputs while typing (#34334) --- .../src/components/editor/IpynbCellRun.tsx | 7 ++-- .../IpynbCellRunOutputs.language.test.tsx | 40 +++++++++++++++++++ .../src/components/editor/IpynbHtmlOutput.tsx | 3 ++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx diff --git a/src/renderer/src/components/editor/IpynbCellRun.tsx b/src/renderer/src/components/editor/IpynbCellRun.tsx index 370588d6b52..c0398398f88 100644 --- a/src/renderer/src/components/editor/IpynbCellRun.tsx +++ b/src/renderer/src/components/editor/IpynbCellRun.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { memo, useMemo } from 'react' import { formatToolDuration } from '../../../../shared/native-chat/tools/native-chat-tool-identity' import { IpynbCellOutputs } from './IpynbCellOutputs' import { IpynbRunPrompt } from './IpynbCellToolbar' @@ -30,7 +30,8 @@ export function IpynbCellRunPrompt({ } /** A cell's outputs: live from its run until the document takes them in, then from the document. */ -export function IpynbCellRunOutputs({ +// Why: every keystroke re-renders the viewer, and re-parsing each cell's saved outputs costs ms. +export const IpynbCellRunOutputs = memo(function IpynbCellRunOutputs({ filePath, cellKey, cell @@ -50,4 +51,4 @@ export function IpynbCellRunOutputs({ [cell, run] ) return -} +}) diff --git a/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx b/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx new file mode 100644 index 00000000000..5d5d2392e0f --- /dev/null +++ b/src/renderer/src/components/editor/IpynbCellRunOutputs.language.test.tsx @@ -0,0 +1,40 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { i18n } from '@/i18n/i18n' +import { IpynbCellRunOutputs } from './IpynbCellRun' +import type { IpynbCell } from './ipynb-parse' + +vi.mock('@/hooks/use-document-dark-theme', () => ({ useDocumentDarkTheme: () => false })) + +const cell: IpynbCell = { + id: 'cell-1', + kind: 'code', + language: 'python', + source: 'df', + executionCount: 1, + outputs: [ + { + kind: 'display', + outputType: 'display_data', + executionCount: null, + items: [{ mime: 'text/html', value: '
' }] + } + ] +} + +afterEach(async () => { + cleanup() + await i18n.changeLanguage('en') +}) + +describe('memoized notebook cell outputs', () => { + it('relabels HTML output when the UI language changes without new props', async () => { + await i18n.changeLanguage('en') + render() + expect(screen.getByTitle('Notebook HTML output')).toBeTruthy() + + await i18n.changeLanguage('es') + expect(await screen.findByTitle('Salida HTML del notebook')).toBeTruthy() + }) +}) diff --git a/src/renderer/src/components/editor/IpynbHtmlOutput.tsx b/src/renderer/src/components/editor/IpynbHtmlOutput.tsx index a60490e4e70..61c9dcbbe13 100644 --- a/src/renderer/src/components/editor/IpynbHtmlOutput.tsx +++ b/src/renderer/src/components/editor/IpynbHtmlOutput.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' import DOMPurify from 'dompurify' +import { useTranslation } from 'react-i18next' import { translate } from '@/i18n/i18n' import { useDocumentDarkTheme } from '@/hooks/use-document-dark-theme' @@ -19,6 +20,8 @@ const OUTPUT_DOCUMENT_HEAD = ` ` export function IpynbHtmlOutput({ html }: { html: string }): React.JSX.Element { + // Why: memoized cell outputs skip parent re-renders, so subscribe to language changes here. + useTranslation() const colorScheme = useDocumentDarkTheme() ? 'dark' : 'light' const srcDoc = useMemo( () =>