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:
parent
dd8bb7aa5c
commit
d90e640e89
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ''
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const { panels } = useI18n()
|
|||
:force-mount="true"
|
||||
:hidden="activeTab !== 'code'"
|
||||
>
|
||||
<CodePanel />
|
||||
<CodePanel :active="activeTab === 'code'" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
|
|
|
|||
33
tests/e2e/code/mobile-panel.spec.ts
Normal file
33
tests/e2e/code/mobile-panel.spec.ts
Normal 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')
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue