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
This commit is contained in:
Danila Poyarkov 2026-08-14 14:24:10 +03:00 committed by GitHub
parent dd8bb7aa5c
commit d90e640e89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 69 additions and 2 deletions

View file

@ -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.

View file

@ -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 ''

View file

@ -190,7 +190,7 @@ const drawerTransition = {
<TabsContent value="code" class="mt-0 h-full data-[state=inactive]:hidden">
<div data-test-id="mobile-drawer-code" class="flex h-full flex-col">
<CodePanel />
<CodePanel :active="isOpen && getDrawerTab() === 'code'" />
</div>
</TabsContent>

View file

@ -62,7 +62,7 @@ const { panels } = useI18n()
:force-mount="true"
:hidden="activeTab !== 'code'"
>
<CodePanel />
<CodePanel :active="activeTab === 'code'" />
</TabsContent>
<TabsContent

View file

@ -0,0 +1,33 @@
import { test, expect, useEditorSetup } from '#tests/e2e/fixtures'
const editor = useEditorSetup()
test.use({ viewport: { width: 390, height: 844 } })
test('closed mobile Code drawer defers JSX until reopened', async () => {
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')
})

View file

@ -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<void>((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()