Skip to content

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

  1. From the screen's root ViewGroup, walk the view tree depth-first.
  2. For each view, check whether it implements one of Pinterest's three opt-in marker interfaces: PerfImageView, PerfTextView, PerfVideoView.
  3. 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.
  4. For each visible PerfImageView / PerfTextView: check isDrawn().
  5. For each visible PerfVideoView: check isVideoLoadStarted().
  6. 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 changesViewTreeObserver.OnGlobalLayoutListener on Android.
  • Draw eventsViewTreeObserver.OnPreDrawListener, OnDrawListener.
  • Frame callbacksChoreographer.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.
  • 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 viewsRecyclerView / 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 BaseSurface walks the view tree from RootView, filters to visible PerfImageView / PerfTextView / PerfVideoView instances, conjoins readiness. Extended to iOS and Web.
Last updated · 319 distilled / 1,201 read