Summary
BodyRenderer holds its PathCache as an instance property:
// Sources/MuscleMap/Rendering/BodyRenderer.swift:62
private let pathCache = PathCache()
but a BodyRenderer is constructed fresh on every draw, so the cache is populated and thrown away each time and never serves a hit. Every render re-runs PathBuilder.buildPath over the full body — a few hundred SVG path strings — on the main thread.
Version: 1.6.4.
Where the renderer is rebuilt
BodyView.standardBody constructs one inside the Canvas draw closure:
// Sources/MuscleMap/Views/BodyView.swift
Canvas { context, size in
makeRenderer(size: size).render(context: &context, size: size)
}
makeRenderer(size:) returns BodyRenderer(...), so each invalidation of that Canvas allocates a new renderer and a new empty PathCache. The same pattern appears in InteractiveBodyOverlay, BodyAccessibilityOverlay, MuscleTooltip and AnimatedBodyView, each with its own short-lived cache — so a single BodyView parses the body several times per frame.
Reproduction
struct ContentView: View {
@State private var tick = 0
var body: some View {
VStack {
// Any unrelated state change invalidates the Canvas and re-parses
// the whole body.
Text("\(tick)")
BodyView(gender: .male, side: .front)
.highlight(.chest, color: .red)
Button("Redraw") { tick += 1 }
}
}
}
Add a counter to PathBuilder.buildPath (or a breakpoint) and it is hit the same number of times on every redraw, rather than only on the first.
The symptom in an app: a screen showing two bodies (front + back) is noticeably slow to appear in a Debug build and stutters while the surrounding view updates.
Suggestion
Make the cache outlive the renderer — a static let shared = PathCache(), or a cache owned by the view and passed in. The existing key already includes scale/offsetX/offsetY, so a shared instance stays correct across sizes; only eviction would need thought.
I worked around it downstream by wrapping the body in an EquatableView so it is only rebuilt when the highlight data actually changes, but that only reduces the frequency — the first render of each distinct state still re-parses everything.
Summary
BodyRendererholds itsPathCacheas an instance property:but a
BodyRendereris constructed fresh on every draw, so the cache is populated and thrown away each time and never serves a hit. Every render re-runsPathBuilder.buildPathover the full body — a few hundred SVG path strings — on the main thread.Version: 1.6.4.
Where the renderer is rebuilt
BodyView.standardBodyconstructs one inside theCanvasdraw closure:makeRenderer(size:)returnsBodyRenderer(...), so each invalidation of thatCanvasallocates a new renderer and a new emptyPathCache. The same pattern appears inInteractiveBodyOverlay,BodyAccessibilityOverlay,MuscleTooltipandAnimatedBodyView, each with its own short-lived cache — so a singleBodyViewparses the body several times per frame.Reproduction
Add a counter to
PathBuilder.buildPath(or a breakpoint) and it is hit the same number of times on every redraw, rather than only on the first.The symptom in an app: a screen showing two bodies (front + back) is noticeably slow to appear in a Debug build and stutters while the surrounding view updates.
Suggestion
Make the cache outlive the renderer — a
static let shared = PathCache(), or a cache owned by the view and passed in. The existing key already includesscale/offsetX/offsetY, so a shared instance stays correct across sizes; only eviction would need thought.I worked around it downstream by wrapping the body in an
EquatableViewso it is only rebuilt when the highlight data actually changes, but that only reduces the frequency — the first render of each distinct state still re-parses everything.