Skip to content

Mobile resume investigation — 2026-09-08

Reported on iPhone Chrome: a blank strip below the page, sometimes with the page shifted upward, and intermittent unresponsive UI after leaving the tab idle. Refresh clears both symptoms. The screenshot alone cannot distinguish stale viewport geometry, a retained outer scroll offset, an orphaned modal lock, or a browser rendering failure.

Findings and changes

  • Viewport lifecycle: the shell previously measured keyboard insets only on resize/scroll/orientation events. It did not handle visibility changes, BFCache pageshow, or focus loss. New DOM regressions reproduced a retained keyboard inset and absent resume updates. The controller now remeasures on resume and focus changes, with bounded follow-up measurements after browser animations settle.
  • Mixed coordinate systems: the keyboard delta came from innerHeight, but CSS subtracted it from 100dvh. Those heights can differ on iOS. The shell now uses one measured height (--mobile-vh), applying the keyboard adjustment once. Browser-chrome filtering, safe-area handling, pinch zoom and invalid-measurement guards remain in place. Real iPhone verification is still needed for toolbar transitions.
  • Outer scroll offsets: keyboard scrolling can move the otherwise locked outer wrappers, hiding the top bar and exposing the page background. The screenshot's dark strip closely matches Yumina's base background, and its top bar is absent, consistent with this hypothesis. Recovery now resets a stable offset in the outer wrappers on resume/focus transitions and when a later outer scroll occurs. It preserves the separate feed/chat/editor scrollers, ignores their scroll events, and defers during active typing, pinch zoom and touch gestures. This reproduces and repairs the DOM state; it does not reproduce a WebKit compositor failure. The reporting account is shared, so no production replay has been identified as the user's occurrence.
  • Orphaned overlay locks: the old cleanup ran only after route changes. New tests reproduced locks surviving same-page return and menu dismissal. Recovery now observes overlay closure/removal and tab return. It waits for exit animations, preserves live nested and custom dialogs, and ignores the closed, permanently mounted mobile drawer and empty popper wrappers.
  • Smoother updates: viewport events are coalesced into one animation frame; unchanged styles are not rewritten. Keyboard polling stops while hidden or after keyboard dismissal. Discover's carousel now stops rotating while hidden and gives the current banner a full reading interval on return, restarting its progress animation. Every listener, timer, observer and probe is cleaned up.
  • Discover crash: PostHog supplied an actionable stack from FeaturedCard through resolveTagLabel to currentTagLocale: i18n.language can be undefined before asynchronous initialization, while React Suspense is disabled. The reproduced failure now falls back to English until the language loads.
  • Diagnostics: added bounded ui_recovery events for viewport geometry changes after resume, corrected outer scroll offsets, and cleared orphaned locks. These record dimensions/reason and known wrapper labels, not input text or game content. A geometry change is a recovery signal, not proof that a user saw a blank strip.

Production evidence

Read-only PostHog queries, project 318450, seven-day window beginning September 1. Counts are investigation snapshots, not a count of reported freezes.

  • Discover language initialization crash: 30 occurrences, 30 sessions, 30 users. Sample: Firefox on Android, /app/hub. Fixed locally.
  • Repeated Chrome iOS stack overflow: 42 occurrences, 3 sessions, 2 users at query time. Sample: Chrome iOS 151, iOS 26.4.2. Alternating Nk/Pk frames refer to page URLs and have invalid source-map responses. A separate hub occurrence has the same pattern. The responsible script is unresolved; these changes do not claim to fix that error. Do not assume it belongs to Yumina, an extension, or Chrome without a usable stack.
  • getComputedStyle error: 1,425 occurrences, 66 sessions, 32 users. Latest samples were desktop Safari with source-less frames. No substantiated connection to this iPhone report; left unchanged.
  • Chat already has bounded fetch/stream deadlines and message refresh on visibility return. No generation requests are retried or cancelled by the UI recovery changes.

Browser references: MDN VisualViewport, WebKit delayed viewport measurements, and iOS 26 viewport regression. These establish relevant browser behavior, not a reproduction on the reporting phone.

Verification

  • Regression tests were observed failing before their respective fixes, then passing: resume geometry, focus loss, outer scroll recovery (including late shifts and touch deferral), same-page orphaned locks, closed mobile drawer, and language initialization.
  • All 23 targeted viewport, overlay and locale tests pass; related route-scroll, language-clamp, SSE and stream-deadline tests also pass in the full suite.
  • pnpm build and pnpm typecheck pass. Build reports the existing large-chunk warnings.
  • After incorporating the latest main, the full app suite passes 720 of 723 tests, with three existing mobile-nav-drawer-layout.test.ts spacing assertion failures. Running that test against copies of all its input files from the investigation's original HEAD reproduced the same three failures; the incoming commits did not change those files. No drawer sizing was changed.
  • No local browser was opened, in accordance with AGENTS.md. No physical iPhone reproduction or production deployment was performed.

Device follow-up after deployment

On the reporting iPhone/Chrome version, open Discover, scroll, then switch apps or lock the phone for 15–30 minutes and return. Repeat from chat with the keyboard open, with a preview/menu open, after closing it, and through browser Back/Forward. Check that the top bar and bottom edge recover, feed position and drafts survive, and tapping/scrolling works. Also check toolbar expansion/collapse, portrait/landscape, manual pinch zoom, and nested dialogs.

If freezing persists, distinguish whether scrolling and menus still respond, correlate the time with ui_recovery and error tracking, and obtain a usable source stack for the Chrome iOS recursion. A JS or compositor stall cannot be proven resolved by DOM tests alone.