Performance improvement for main-thread NodeRenderSystem #2662
ChrisBenua
started this conversation in
General
Replies: 1 comment 1 reply
-
|
This is nice analysis. I would be happy to review a PR with these sorts of optimizations, assuming the changes aren't too invasive. |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Problem Summary
In large apps, Swift runtime protocol conformance checks can significantly impact startup time and app responsiveness. This issue documents profiling results and proposes concrete fixes to reduce casting overhead in Lottie's NodeRenderSystem.
Background: Swift Protocol Casting Costs
The first conformance check for any given
(type, protocol)pair is expensive because the runtime must scan all protocol conformance type descriptors. After a successful cast, Swift caches the protocol witness table for that(type, protocol)pair - but in apps that initialize Lottie animations at startup, repeated casting across many types can still add up to measurable overhead.Profiling Results
Profiling was performed in our app on iOS Simulator and iPhone 13 (Release build).
The majority of this time was spent inside GroupNode.init and initializeNodeTree, as shown below:
After applying proposed fixes all overhead from type-casting is gone
Root Cause
There are two categories of protocol casting happening inside the
NodeRenderSystem:Explicit casts in (ItemExtension.swift)
if let pathNode = nodeTree.rootNode as? PathNode { ... }if let renderNode = nodeTree.rootNode as? RenderNode { ... }Implicit casts in (ShapeCompositionLayer.swift and GroupNode.swift)
childKeypaths.append(contentsOf: results.childrenNodes)Proposed Fixes
Fix 1: Replace protocol casts with exhaustive type switches in ItemsExtension.swift
Since
PathNodeandRenderNodeare internal protocols with a closed, known set of conforming types , we can replace runtime protocol casting with concrete type switches. This avoids conformance scanning entirely.And update
initializeNodeTreeto use these helpers:Tradeoffs:
This approach violates the Open/Closed Principle. Any new type conforming to
PathNodeorRenderNodemust also be added to these switch statements. A compile-time exhaustiveness check or a documentation comment on the protocols would help mitigate this risk.Fix 2: Explicit element-wise boxing to avoid
_arrayForceCastReplacing implicit array covariance casts with an explicit
.map { $0 as KeypathSearchable }causes the compiler to emit a direct existential box operation per element, completely bypassing _arrayForceCast and its per-element conformance checks.Why this works: The explicit as
KeypathSearchablecast on a value already known to be a concrete conforming type is resolved at compile time. The.mapintroduces a small allocation, but this is far cheaper than repeated conformance table scanning.Beta Was this translation helpful? Give feedback.
All reactions