PATTERN Cited by 1 source
View-tree walk for readiness detection¶
Problem¶
A modern UI screen is composed of many elements (images, text, videos, chrome). The user's "is the screen ready" predicate — what Pinterest calls Visually Complete — depends on the state of specific content-critical elements, not on any single framework-level signal. No single API fires "all the images rendered and videos playing" automatically. Hand-rolling per-screen detectors is expensive (two engineer-weeks per surface on Android per Pinterest's 2026-04-08 post) and gates coverage.
Solution¶
Walk the UI element tree from a common root and compute the readiness predicate by inspecting a uniform interface on each node. The tree walk is mechanical; the per-element readiness logic is factored into interfaces implemented by the leaf views. Composition (conjunction over all visible, opted-in nodes) yields the per-screen predicate without per-screen code.
Pinterest's exact mechanism (Source: sources/2026-04-08-pinterest-performance-for-everyone):
- From the screen's root
ViewGroup, walk the view tree depth-first. - For each view, check whether it implements one of Pinterest's three opt-in marker interfaces:
PerfImageView,PerfTextView,PerfVideoView. - Filter to visible views using the geometry methods on the interface (
x(),y(),width(),height()) — off-screen-but-in-tree views don't block completion. - For each visible
PerfImageView/PerfTextView: checkisDrawn(). - For each visible
PerfVideoView: checkisVideoLoadStarted(). - Conjunction: Visually Complete fires when all visible opted-in views report ready.
"At the BaseSurface level, given that we should have access to the root android ViewGroup (e.g. RootView), we could just iterate through the view tree starting from the RootView by visiting all the views on this tree. We will focus on those visible views and judge if all the PerfImageView, PerfTextView and PerfVideoView instances are all drawn or started if it's a video." (Source: sources/2026-04-08-pinterest-performance-for-everyone).
Cadence and triggering¶
The post doesn't specify when the walk runs. Practical triggers:
- Layout changes —
ViewTreeObserver.OnGlobalLayoutListeneron Android. - Draw events —
ViewTreeObserver.OnPreDrawListener,OnDrawListener. - Frame callbacks —
Choreographer.postFrameCallback(Android),CADisplayLink(iOS),requestAnimationFrame(web). - Debounced polling — fallback when event-driven triggers are noisy.
A naive every-frame walk is expensive; practical implementations batch, debounce, or hook layout-settled signals.
Cost characteristics¶
- O(|tree|) per walk. Deeply nested or view-heavy screens (feeds with many cards × many images per card) can have hundreds of views.
- Probe cost dominated by the
isDrawn()/isVideoLoadStarted()/ geometry method calls, not the iteration itself. - Non-perturbation is a design requirement — the walk must not materially slow the thing it's measuring.
When to use¶
- There's a UI element tree (Android
View, iOS UIKit, DOM, Flutter widget, etc.) and screens share a common root. - The per-screen "ready" state can be decomposed into per-element readiness plus composition logic.
- Product engineers can tag content-critical elements via a small opt-in interface (see patterns/opt-in-performance-interface).
- The platform wants uniform per-screen measurement without per-screen code.
When not to use¶
- No natural tree — event-stream-driven UIs or non-hierarchical composition (some ECS-style game UIs).
- Readiness is not a per-element property — if done-state is defined by animation completion, scroll stability, or user-level events, readiness isn't derivable from per-view state.
- Tree is too deep / traversal too slow — if the O(|tree|) cost is prohibitive, targeted observation (layout listeners on specific views) wins.
Related mechanisms¶
IntersectionObserver(web) — per-element visibility observation; composable with this pattern for the visibility filter step.- Android
ViewTreeObserver— lifecycle-aware hooks for the traversal. - iOS
UIViewController.viewDidAppear— coarse readiness signal; typically paired with per-view observation. - Largest Contentful Paint (LCP) (web Core Web Vitals) — browser-level heuristic for the same predicate, but chooses one "largest" element rather than conjoining over many.
Caveats¶
- Dynamic trees — views appear/disappear during layout / animations / scroll. A snapshot walk can miss intermediate states.
- Lazy-loaded / recycled views —
RecyclerView/UICollectionView/ virtualised DOM may keep views off-tree when logically visible. A "visible" filter via geometry can miss logically-visible-but-not-materialised content. - Opaque overlay coverage — a view may be geometrically on-screen but covered by a modal or splash; geometry-based visibility filters miss this.
- Tagging correctness — the walk only sees what's tagged; under-tagging produces false early completion, over-tagging produces never-complete.
Seen in¶
- 2026-04-08 Pinterest — Performance for Everyone (sources/2026-04-08-pinterest-performance-for-everyone) — canonical wiki instance. Pinterest Android
BaseSurfacewalks the view tree fromRootView, filters to visiblePerfImageView/PerfTextView/PerfVideoViewinstances, conjoins readiness. Extended to iOS and Web.
Related¶
- concepts/view-tree-traversal
- concepts/visually-complete
- concepts/opt-in-marker-interface
- patterns/base-class-automatic-instrumentation — companion pattern; the base class is where the walk lives
- patterns/opt-in-performance-interface — companion pattern; how the walk knows which views to consider
- systems/pinterest-base-surface
- systems/pinterest-perf-view