Politur: Performance-Bottleneck (ctx.measureText) durch clientseitiges Text-Breiten-Caching auf dem Knoten geloest

This commit is contained in:
Hitonabi
2026-07-09 12:41:16 +02:00
parent 082f4a659d
commit 65d5a49411
4 changed files with 29 additions and 23 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-Bz6q5kVA.js"></script> <script type="module" crossorigin src="/assets/index-D663kYCx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index--2fz5jZz.css"> <link rel="stylesheet" crossorigin href="/assets/index--2fz5jZz.css">
</head> </head>
<body> <body>
+17 -11
View File
@@ -61,7 +61,7 @@ export function GraphView({ data, selectedNodeId, onNodeSelect }: {
} }
}, [data]) }, [data])
// Grad (Degree) berechnen fängt D3-Objektmutationen robust ab! // Grad (Degree) berechnen fängt D3-Objektmutationen robust ab
const degree = useMemo(() => { const degree = useMemo(() => {
const d: Record<string, number> = {} const d: Record<string, number> = {}
data.edges.forEach((e) => { data.edges.forEach((e) => {
@@ -75,7 +75,7 @@ export function GraphView({ data, selectedNodeId, onNodeSelect }: {
return d return d
}, [data]) }, [data])
// Quadranten-Fokuskoordinaten für sauberes Clustering (Fokuszentren weiter auseinander gezogen) // Quadranten-Fokuskoordinaten für sauberes Clustering
const getClusterX = (category: string) => { const getClusterX = (category: string) => {
return { return {
identity: -240, identity: -240,
@@ -132,7 +132,7 @@ export function GraphView({ data, selectedNodeId, onNodeSelect }: {
fgRef.current.zoomToFit(400, 40) fgRef.current.zoomToFit(400, 40)
} }
// Nachbarmenge für extrem schnellen Lookup fängt D3-Objektmutationen robust ab! // Nachbarmenge für extrem schnellen Lookup fängt D3-Objektmutationen robust ab
const connectedNodes = useMemo(() => { const connectedNodes = useMemo(() => {
if (!activeNode) return new Set<string>() if (!activeNode) return new Set<string>()
const set = new Set<string>() const set = new Set<string>()
@@ -157,15 +157,11 @@ export function GraphView({ data, selectedNodeId, onNodeSelect }: {
backgroundColor="rgba(0,0,0,0)" backgroundColor="rgba(0,0,0,0)"
cooldownTicks={120} cooldownTicks={120}
// Exakte Hit-Map auf unsichtbarem Canvas zeichnen fängt D3-Objektmutationen robust ab! // Ultraschneller, statischer Klick- & Hover-Puffer auf dem Hit-Canvas (verhindert teure Echtzeitrechnungen)
nodePointerAreaPaint={(node: any, color: string, ctx: CanvasRenderingContext2D) => { nodePointerAreaPaint={(node: any, color: string, ctx: CanvasRenderingContext2D) => {
const x = node.x ?? 0
const y = node.y ?? 0
const nId = typeof node === "object" ? node.id : node
const size = 6 + Math.min(degree[nId] || 0, 15) * 0.8
ctx.fillStyle = color ctx.fillStyle = color
ctx.beginPath() ctx.beginPath()
ctx.arc(x, y, size + 5, 0, 2 * Math.PI, false) // 5px Puffer für einfaches Treffen ctx.arc(node.x ?? 0, node.y ?? 0, 16, 0, 2 * Math.PI, false)
ctx.fill() ctx.fill()
}} }}
@@ -239,16 +235,26 @@ export function GraphView({ data, selectedNodeId, onNodeSelect }: {
} }
// 2. Zeichne das Label nur unter bestimmten Bedingungen (Zoom-abhängig) // 2. Zeichne das Label nur unter bestimmten Bedingungen (Zoom-abhängig)
// Verhindert Text-Clutter und macht Knotenpunkte permanent sichtbar.
const showLabel = isHovered || isSelected || (globalScale >= 1.25) || (activeNode && connectedNodes.has(node.id)) const showLabel = isHovered || isSelected || (globalScale >= 1.25) || (activeNode && connectedNodes.has(node.id))
if (showLabel) { if (showLabel) {
const label = node.content.length > 35 ? node.content.slice(0, 34) + "…" : node.content const label = node.content.length > 35 ? node.content.slice(0, 34) + "…" : node.content
// Textbreite EINMALIG auf dem Objekt cachen (verhindert massive Frame-Einbrüche durch ctx.measureText)
if (node.__baseTextWidth === undefined) {
const prevFont = ctx.font
ctx.font = '500 10px "Space Grotesk", sans-serif'
node.__baseTextWidth = ctx.measureText(label).width
ctx.font = prevFont
}
const baseFontSize = isHovered || isSelected ? 10.5 : 9.5 const baseFontSize = isHovered || isSelected ? 10.5 : 9.5
const fontSize = baseFontSize / Math.max(0.5, globalScale) const fontSize = baseFontSize / Math.max(0.5, globalScale)
ctx.font = `${isHovered || isSelected ? "bold" : "500"} ${fontSize}px "Space Grotesk", sans-serif` ctx.font = `${isHovered || isSelected ? "bold" : "500"} ${fontSize}px "Space Grotesk", sans-serif`
const textWidth = ctx.measureText(label).width
// Breite linear zur geänderten FontSize skalieren KEIN ctx.measureText Aufruf mehr pro Frame!
const textWidth = node.__baseTextWidth * (fontSize / 10)
const bpad = 4.5 / Math.max(0.5, globalScale) const bpad = 4.5 / Math.max(0.5, globalScale)
ctx.fillStyle = isDimmed ? "rgba(10, 15, 25, 0.45)" : "rgba(8, 12, 20, 0.88)" ctx.fillStyle = isDimmed ? "rgba(10, 15, 25, 0.45)" : "rgba(8, 12, 20, 0.88)"