fix(minimap): minimap re-render/perf issue (#9741)

## Summary

Fix #9732

To clarify how preventing the 60 FPS object assignment solves the
`vue-i18n` (intlify) issue, here is the complete chain reaction leading
to the performance loop:

1. The Root Cause: In `useMinimapViewport.ts`, `useRafFn` acts as a
timer bound to the browser's **refresh rate** (60 FPS). In the original
code, it unconditionally executed the `viewportTransform.value = { ... }
`assignment 60 times a second.

2. Vue's Reactivity Interception: Because `viewportTransform` is a
reactive variable (`ref`), updating it causes its corresponding
**computed** property (`viewportStyles`) to register a data dependency
update.

3. Forced Re-rendering: The `<template> ` in `MiniMap.vue` is bound to
`:style="viewportStyles"`. Since the dependent value changed, Vue's
Virtual DOM decides: "I must re-render the entire `MiniMap.vue`
interface **60 times** per second to ensure the element positions are
up-to-date!"

4. The Victim Emerges: Inside the template of `MiniMap.vue`, there are
several internationalization translation functions: `<button
:aria-label="$t('g.settings')" ...> <button :aria-label="$t('g.close')"
...> `In Vue, whenever a component re-renders, all functions within its
template (including `$t()`) must be re-evaluated. Because the component
was being forced to re-render **60 times** per second, and there are
approximately **6 calls** to `$t()` within this UI, it multiplied into
60 × 6 = **360** intlify compilation and evaluate events per second.


## Solution
Only assemble objects and hand them over to Vue for rendering when the
mouse is actually dragging the canvas.

By extracting the math into **stack-allocated** primitive variables `(x,
y, w, h) `and strictly comparing them, it completely halts the CPU burn
at the source with minimal runtime overhead.

## Screenshot

before
<img width="1820" height="908" alt="image"
src="https://github.com/user-attachments/assets/b48d1e76-6498-47c0-af41-e0594d4e7e2f"
/>

after
<img width="1566" height="486" alt="image"
src="https://github.com/user-attachments/assets/5848b7b7-c57c-494f-a99e-4f7c92889ed0"
/>
This commit is contained in:
Kelly Yang
2026-03-11 17:20:51 -07:00
committed by GitHub
parent f5088d6cfb
commit aadec87bff
2 changed files with 50 additions and 6 deletions

View File

@@ -187,6 +187,46 @@ describe('useMinimapViewport', () => {
expect(transform.height).toBeCloseTo(viewportHeight * 0.5) // 300 * 0.5 = 150
})
it('should maintain strict reference equality for viewportTransform when canvas state is unchanged', () => {
vi.mocked(calculateNodeBounds).mockReturnValue({
minX: 0,
minY: 0,
maxX: 500,
maxY: 400,
width: 500,
height: 400
})
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
vi.mocked(calculateMinimapScale).mockReturnValue(0.5)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
mockCanvas.ds.scale = 2
mockCanvas.ds.offset = [-100, -50]
viewport.updateBounds()
viewport.updateCanvasDimensions()
viewport.updateViewport()
const initialTransform = viewport.viewportTransform.value
viewport.updateViewport()
const transformAfterIdle = viewport.viewportTransform.value
expect(transformAfterIdle).toBe(initialTransform)
mockCanvas.ds.offset = [-150, -50]
viewport.updateViewport()
const transformAfterPan = viewport.viewportTransform.value
expect(transformAfterPan).not.toBe(initialTransform)
expect(transformAfterPan.x).not.toBe(initialTransform.x)
})
it('should center view on world coordinates', () => {
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>

View File

@@ -90,12 +90,16 @@ export function useMinimapViewport(
const centerOffsetX = (width - bounds.value.width * scale.value) / 2
const centerOffsetY = (height - bounds.value.height * scale.value) / 2
viewportTransform.value = {
x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,
y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,
width: viewportWidth * scale.value,
height: viewportHeight * scale.value
}
const x = (worldX - bounds.value.minX) * scale.value + centerOffsetX
const y = (worldY - bounds.value.minY) * scale.value + centerOffsetY
const w = viewportWidth * scale.value
const h = viewportHeight * scale.value
const curr = viewportTransform.value
if (curr.x === x && curr.y === y && curr.width === w && curr.height === h)
return
viewportTransform.value = { x, y, width: w, height: h }
}
const updateBounds = () => {