Skip to content

Non-deterministic widget trees break incremental diff caching #199

@jappeace-sloth

Description

@jappeace-sloth

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions