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