From d90e640e8991cfc03d587b6bb705ae8e1f6f1598 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 14 Aug 2026 14:24:10 +0300 Subject: [PATCH] perf(app): defer inactive Code panel generation (#514) * perf(app): defer inactive Code panel generation - Skip JSX serialization and syntax highlighting while the Code tab is hidden - Restore code generation when desktop or mobile users activate the tab - Cover large Design-tab selections without hidden Code-panel work * test(app): cover deferred Code panel updates - Keep mobile JSX generation inactive while the drawer is closed - Measure the complete two-frame inactive selection flow - Verify mobile Code output refreshes when the drawer reopens --- CHANGELOG.md | 1 + src/components/CodePanel.vue | 2 ++ src/components/MobileDrawer.vue | 2 +- src/components/PropertiesPanel.vue | 2 +- tests/e2e/code/mobile-panel.spec.ts | 33 +++++++++++++++++++++++++++++ tests/e2e/code/panel.spec.ts | 31 +++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/code/mobile-panel.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd4e0e24..5f5c9a10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Performance +- Defer JSX generation and syntax highlighting until the Code panel is active, keeping large canvas selections responsive. (#500) - Index Figma clipboard children once during import instead of rescanning every pasted node, keeping large flat pastes linear. (#500) - Reduce peak memory during `.fig` export by sharing immutable binary resources with the isolated export graph. diff --git a/src/components/CodePanel.vue b/src/components/CodePanel.vue index e43379859..d9557b237 100644 --- a/src/components/CodePanel.vue +++ b/src/components/CodePanel.vue @@ -14,6 +14,7 @@ import Tip from '@/components/ui/Tip.vue' import type { JSXFormat } from '@open-pencil/core/design-jsx' +const { active = true } = defineProps<{ active?: boolean }>() const store = useEditorStore() const { copy, copied } = useClipboard({ copiedDuring: 2000 }) const { dialogs } = useI18n() @@ -29,6 +30,7 @@ function toggleFormat() { } const jsxCode = useSceneComputed(() => { + if (!active) return '' void store.state.sceneVersion const ids = [...store.state.selectedIds] if (ids.length === 0) return '' diff --git a/src/components/MobileDrawer.vue b/src/components/MobileDrawer.vue index af5cd1b1e..518e0a95b 100644 --- a/src/components/MobileDrawer.vue +++ b/src/components/MobileDrawer.vue @@ -190,7 +190,7 @@ const drawerTransition = {
- +
diff --git a/src/components/PropertiesPanel.vue b/src/components/PropertiesPanel.vue index 3ccd96c32..49e837dbd 100644 --- a/src/components/PropertiesPanel.vue +++ b/src/components/PropertiesPanel.vue @@ -62,7 +62,7 @@ const { panels } = useI18n() :force-mount="true" :hidden="activeTab !== 'code'" > - + { + await editor.page.evaluate(() => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + const frameId = store.createShape('FRAME', 0, 0, 100, 100) + store.select([frameId]) + }) + + await editor.page.getByTestId('mobile-ribbon-code').click() + await expect(editor.page.getByTestId('code-panel')).toBeVisible() + + await editor.page.getByTestId('mobile-ribbon-code').click() + await expect + .poll(() => editor.page.evaluate(() => window.openPencil?.getStore?.().state.mobileDrawerSnap)) + .toBe('closed') + + await editor.page.evaluate(() => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + store.clearSelection() + const rectangleId = store.createShape('RECTANGLE', 120, 0, 100, 100) + store.select([rectangleId]) + }) + + await editor.page.getByTestId('mobile-ribbon-code').click() + await expect(editor.page.getByTestId('code-panel')).toContainText('Rectangle') +}) diff --git a/tests/e2e/code/panel.spec.ts b/tests/e2e/code/panel.spec.ts index 0d0bdc05c..dc3df10c0 100644 --- a/tests/e2e/code/panel.spec.ts +++ b/tests/e2e/code/panel.spec.ts @@ -26,6 +26,37 @@ function copyButton() { return editor.page.getByTestId('code-panel-copy') } +test('inactive Code tab skips JSX generation for large selections', async () => { + const selectionDuration = await editor.page.evaluate(async () => { + const store = window.openPencil?.getStore?.() + if (!store) throw new Error('OpenPencil store not initialized') + const pageId = store.state.currentPageId + const ids: string[] = [] + for (let frameIndex = 0; frameIndex < 50; frameIndex++) { + const frame = store.graph.createNode('FRAME', pageId, { name: `Frame ${frameIndex}` }) + ids.push(frame.id) + for (let childIndex = 0; childIndex < 100; childIndex++) { + store.graph.createNode('RECTANGLE', frame.id, { name: `Child ${childIndex}` }) + } + } + const startedAt = performance.now() + store.select(ids) + await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + }) + return performance.now() - startedAt + }) + + expect(selectionDuration).toBeLessThan(1000) + await expect(designTab()).toHaveAttribute('data-state', 'active') + + await codeTab().click() + await expect(codePanel()).toContainText('Frame') + + await editor.page.evaluate(() => window.openPencil?.getStore?.().clearSelection()) + await designTab().click() +}) + test('Code tab shows empty state with no selection', async () => { await codeTab().click() await expect(codePanelEmpty()).toBeVisible()