fix: skip redundant appScalePercentage updates during zoom/pan (#9403)

## What
Add equality check before updating `appScalePercentage` reactive ref.

## Why
Firefox profiler shows 586 `setElementText` markers from continuous text
interpolation updates during zoom/pan. The rounded percentage value
often doesn't change between events.

## How
Extract `updateAppScalePercentage()` helper with equality guard —
compares new rounded value to current before assigning to the ref.

## Perf Impact
Expected: eliminates ~90% of `setElementText` markers during zoom/pan

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9403-fix-skip-redundant-appScalePercentage-updates-during-zoom-pan-31a6d73d3650812db8f2d68ac73c95b0)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2026-03-14 21:44:44 -07:00
committed by GitHub
parent 4781775a78
commit 585e6f87fa
2 changed files with 93 additions and 3 deletions

View File

@@ -0,0 +1,87 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
isAppMode: { value: false },
setMode: vi.fn()
})
}))
vi.mock('@/scripts/app', () => ({
app: {
canvas: {
ds: {
scale: 1,
offset: [0, 0] as [number, number],
onChanged: undefined as
| ((scale: number, offset: [number, number]) => void)
| undefined,
element: null,
changeScale: vi.fn()
},
setDirty: vi.fn(),
graph: null,
selectedItems: new Set(),
subgraph: undefined,
canvas: {
addEventListener: vi.fn(),
removeEventListener: vi.fn()
}
}
}
}))
describe('useCanvasStore', () => {
let store: ReturnType<typeof useCanvasStore>
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
store = useCanvasStore()
vi.clearAllMocks()
})
describe('appScalePercentage', () => {
it('rounds scale to integer percentage', async () => {
const { app } = await import('@/scripts/app')
app.canvas.ds.scale = 1.004
store.initScaleSync()
expect(store.appScalePercentage).toBe(100)
app.canvas.ds.scale = 1.506
app.canvas.ds.onChanged!(app.canvas.ds.scale, app.canvas.ds.offset)
expect(store.appScalePercentage).toBe(151)
})
it('updates reactive value when rounded scale changes', async () => {
const { app } = await import('@/scripts/app')
app.canvas.ds.scale = 1.0
store.initScaleSync()
expect(store.appScalePercentage).toBe(100)
app.canvas.ds.scale = 1.5
app.canvas.ds.onChanged!(app.canvas.ds.scale, app.canvas.ds.offset)
expect(store.appScalePercentage).toBe(150)
})
it('preserves original onChanged handler', async () => {
const { app } = await import('@/scripts/app')
const originalHandler = vi.fn()
app.canvas.ds.onChanged = originalHandler
app.canvas.ds.scale = 1.0
store.initScaleSync()
app.canvas.ds.scale = 2.0
app.canvas.ds.onChanged!(app.canvas.ds.scale, app.canvas.ds.offset)
expect(originalHandler).toHaveBeenCalledWith(2.0, app.canvas.ds.offset)
})
})
})

View File

@@ -43,6 +43,9 @@ export const useCanvasStore = defineStore('canvas', () => {
// Reactive scale percentage that syncs with app.canvas.ds.scale
const appScalePercentage = ref(100)
const updateAppScalePercentage = (scale: number) => {
appScalePercentage.value = Math.round(scale * 100)
}
const { isAppMode, setMode } = useAppMode()
const linearMode = computed({
@@ -59,12 +62,12 @@ export const useCanvasStore = defineStore('canvas', () => {
if (app.canvas?.ds) {
// Initial sync
originalOnChanged = app.canvas.ds.onChanged
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
updateAppScalePercentage(app.canvas.ds.scale)
// Set up continuous sync
app.canvas.ds.onChanged = () => {
if (app.canvas?.ds?.scale) {
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
updateAppScalePercentage(app.canvas.ds.scale)
}
// Call original handler if exists
originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
@@ -106,7 +109,7 @@ export const useCanvasStore = defineStore('canvas', () => {
app.canvas.setDirty(true, true)
// Update reactive value immediately for UI consistency
appScalePercentage.value = Math.round(newScale * 100)
updateAppScalePercentage(newScale)
}
const currentGraph = shallowRef<LGraph | null>(null)