Problem
The render engine's incremental diff (diffRenderNode) relies on widget Eq to detect unchanged subtrees and reuse native nodes. If a widget tree contains non-deterministic values (e.g. newStdGen producing different random positions each render), every re-render produces a structurally different tree. The diff sees no matches, destroys all old nodes, and creates new ones every frame.
This is especially dangerous with Animated wrappers: animation frames trigger renderView at ~60fps, so a non-deterministic subtree causes destroy+create cycles that quickly exhaust the native node pool (256 max on Android).
Reproduction
-- BAD: generates different random positions each render
confettiOverlay :: IO Widget
confettiOverlay = do
gen <- newStdGen -- new entropy each call!
let positions = take 20 (randomRs (-150, 150) gen)
...
When wrapped in Animated, the animation loop calls renderView each vsync. Each call regenerates confettiOverlay with new random positions → full tree mismatch → node pool exhaustion.
Mitigation
Consumers should ensure widget trees are deterministic across re-renders. For randomized content, seed a PRNG once (e.g. at app boot) and derive all random values from that seed, so the same seed always produces the same widget tree.
Possible framework-level improvements
- Document this constraint prominently (widget trees must be deterministic for diff correctness)
- Consider a warning/assertion when too many nodes are created in a single frame
- Node pool could be dynamically sized instead of fixed at 256
Discovered in jappeace/prrrrrrrrr while fixing first-render animation (#191).
Problem
The render engine's incremental diff (
diffRenderNode) relies on widgetEqto detect unchanged subtrees and reuse native nodes. If a widget tree contains non-deterministic values (e.g.newStdGenproducing different random positions each render), every re-render produces a structurally different tree. The diff sees no matches, destroys all old nodes, and creates new ones every frame.This is especially dangerous with
Animatedwrappers: animation frames triggerrenderViewat ~60fps, so a non-deterministic subtree causes destroy+create cycles that quickly exhaust the native node pool (256 max on Android).Reproduction
When wrapped in
Animated, the animation loop callsrenderVieweach vsync. Each call regeneratesconfettiOverlaywith new random positions → full tree mismatch → node pool exhaustion.Mitigation
Consumers should ensure widget trees are deterministic across re-renders. For randomized content, seed a PRNG once (e.g. at app boot) and derive all random values from that seed, so the same seed always produces the same widget tree.
Possible framework-level improvements
Discovered in jappeace/prrrrrrrrr while fixing first-render animation (#191).