Skip to content

fix: keep transitions consistent under browser-back and content fetching#152

Open
kimjh96 wants to merge 2 commits into
mainfrom
fix/transition-robustness
Open

fix: keep transitions consistent under browser-back and content fetching#152
kimjh96 wants to merge 2 commits into
mainfrom
fix/transition-robustness

Conversation

@kimjh96

@kimjh96 kimjh96 commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Two root-cause fixes (not band-aids) for transitions not running as defined on iOS Safari. Both follow one principle: everything the compiled CSS rule keys on must commit in one React paint, and the animation start must not wait on the entering screen's content.

What

1. Shared bar rides a frame late on the browser back button.
Screen A has a shared bar, B does not; from B you go back to A and A's bar must ride along. With a programmatic pop() it was in sync; with the browser back button the bar lagged the screen by a frame. The bar's compiled ride rule keys on data-flemo-bar-status (rendered) AND data-flemo-bar-riding, which was written imperatively by a layout effect (driveBarRiding). On a genuine popstate, React reconnects the unfrozen screen's effects as follow-up work, so the riding flag could present a frame after the status. Now computeBarRiding (a pure function in @flemo/core) decides the ride and ScreenMotion renders data-flemo-bar-riding on the same element as the status, so all four attributes commit together for any trigger and any transition.

2. Transition pushed late / stutters when the entering screen fetches.
The scope the keyframe animates and the consumer's {children} committed in one render, so a heavy cold mount (lazy chunk + fetch + large DOM) delayed the first paint, and thus the animation's start, until that whole subtree committed. First (cold) visit janked, second (warm) was smooth. ScreenMotion now paints the scope first over an empty content box (the keyframe starts at once, over a cheap layer) and fills {children} on the next, transition-priority commit. Gated to an entering push/replace screen that its initial hides on the first frame; root, SSR, pop (Activity-preserved), and none render children directly.

Why

Both are timing/compositor issues that survived the earlier translateZ content isolation, worst on a real iPhone GPU. Root-caused by tracing the React-commit / store-subscription ordering for each path, not by guessing.

Impact

  • @flemo/core minor: the internal driveBarRiding engine helper is replaced by the pure computeBarRiding. No documented consumer API changes.
  • @flemo/react patch: both fixes are transparent to consumers (<Screen>{anything}</Screen> is unchanged).

Test plan

  • pnpm turbo run typecheck lint test build green (14/14).
  • New: computeBarRiding unit tests; Screen tests asserting the bar's data-flemo-bar-riding co-renders with its status (rides when the partner lacks the bar, not when it owns it) and that a none-transition entering screen renders content immediately.
  • ACCEPTANCE is iOS Safari on a real iPhone: install this PR's pkg.pr.new preview package into plen and confirm (a) the bar no longer lags on browser-back and (b) the cold first-visit transition runs cleanly. Desktop WebKit under-reports both (tile-raster cost differs), so the device is the gate.

Rollback

Revert the squash merge. No consumer code changes either way.

Two root-cause fixes for transitions not running as defined on iOS Safari,
both grounded in the same principle: everything the compiled CSS rule keys on
must commit in one React paint, and the animation's start must not wait on the
entering screen's content.

1. Shared bar rides a frame late on the browser back button. The bar's CSS ride
   rule keys on data-flemo-bar-status (rendered) AND data-flemo-bar-riding,
   which used to be written imperatively by a layout-effect (driveBarRiding).
   On a genuine popstate, React reconnects the unfrozen screen's effects as
   follow-up work, so the riding flag could land a frame after the status. Now
   computeBarRiding (pure, in @flemo/core) decides the ride and ScreenMotion
   renders data-flemo-bar-riding on the same element as the status, so they
   commit together for any trigger and any transition.

2. Transition pushed late / stutters when the entering screen fetches. The
   scope the keyframe animates and the consumer's children committed together,
   so a heavy cold mount (lazy chunk + fetch + large DOM) delayed the first
   paint, and thus the animation start, until that whole subtree committed.
   ScreenMotion now paints the scope first over an empty content box (the
   keyframe starts at once, over a cheap layer) and fills the children on the
   next, transition-priority commit. Gated to an entering push/replace screen
   hidden by its initial offset; root/SSR/pop/none render children directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flemo-web Ready Ready Preview, Comment Jun 30, 2026 4:20pm

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e16ec6b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@flemo/core Minor
@flemo/react Patch
@flemo/react-layout Patch
@flemo/web Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
@flemo/core 11.54 KB (-0.5% 🔽)
@flemo/react 7.01 KB (+0.59% 🔺)
@flemo/react-layout 552 B (0%)

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@flemo/core@152
npm i https://pkg.pr.new/@flemo/react@152
npm i https://pkg.pr.new/@flemo/react-layout@152

commit: e16ec6b

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.19%. Comparing base (39c3b51) to head (e16ec6b).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #152      +/-   ##
==========================================
+ Coverage   84.05%   84.19%   +0.14%     
==========================================
  Files          69       70       +1     
  Lines        1254     1240      -14     
  Branches      294      294              
==========================================
- Hits         1054     1044      -10     
  Misses        119      119              
+ Partials       81       77       -4     
Flag Coverage Δ
unittests 84.19% <100.00%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/core/src/screen/computeBarRiding.ts 100.00% <100.00%> (ø)
packages/core/src/transition/initialHidesScreen.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…n enter

Refine the content-deferral gate. It keyed on "initial is non-empty", which
also deferred transitions that stay visible on the first frame (e.g. the layout
preset's opacity 0.97, or a scale), flashing a solid empty box. Extract the pure
`initialHidesScreen` (fully transparent, or translated off-screen by >=100%) so
deferral applies only when the empty box is invisible; partial fades/scales
render content immediately as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant