Skip to content

perf(app): stop VoiceBars loops on unmount and share one ShimmerText sweep driver - #1811

Open
f-liva wants to merge 2 commits into
slopus:mainfrom
f-liva:perf/animation-cleanups
Open

f-liva wants to merge 2 commits into
slopus:mainfrom
f-liva:perf/animation-cleanups

Conversation

@f-liva

@f-liva f-liva commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Two animation loops that outlive the component that started them.

VoiceBars (sources/components/VoiceBars.tsx) starts three Animated.loop chains in an effect whose else branch stops them when isActive turns false. VoiceAssistantStatusBar mounts the component only while the assistant is speaking, so on unmount the else branch never ran and the loops were never stopped. On web react-native-web's Animated uses the JS driver, where each loop is a requestAnimationFrame chain that restarts forever (iterations: -1): every speech turn leaked three 60 fps rAF loops, their closures and Animated.Values for the life of the tab. The effect now returns a cleanup that calls stopAnimation() on the three values; the else branch is kept for the mounted case.

ShimmerText (sources/components/ShimmerText.tsx, the native implementation) ran one infinite withRepeat loop per instance, one per visible thinking row for as long as the row stayed on screen. The sweep progress now lives in a single module-level makeMutable shared value created lazily (not at import time): the first mounted instance starts the loop, the last one to unmount cancels it (ref-counted), and each useAnimatedStyle just reads the shared value. Same shape as the shared elapsed-time ticker in #1776. Web is untouched: Metro resolves ShimmerText.web.tsx there, a pure CSS shimmer, so this part only affects native.

Related: #1796 (StatusDot pulse cancelAnimation on unmount) and #1798 (ShimmerView repeat cancelAnimation on unmount) are the same family of animations left running after unmount.

Changes

  • Stop the three VoiceBars Animated.loop chains in the effect cleanup so a conditional unmount cannot leave them running
  • Drive every ShimmerText instance from one shared, ref-counted makeMutable sweep value instead of one withRepeat loop per instance

Test plan

  • pnpm typecheck (clean)
  • pnpm exec vitest run sources/hooks/useNavigateToSession.test.ts sources/hooks/useImagePicker.test.ts (37 passed)
  • Ran on a self-hosted web deployment: the voice status bar mounts and unmounts across speech turns without leaving rAF loops behind. The ShimmerText change is native-only (web resolves ShimmerText.web.tsx) and was not exercised on that deployment; it typechecks and follows the same start-on-first-mount / cancel-on-last-unmount pattern as perf(hooks): drive useElapsedTime from a single shared ticker #1776

🤖 Generated with Claude Code

Federico Liva and others added 2 commits September 17, 2026 12:44
VoiceAssistantStatusBar mounts VoiceBars only while the assistant is
speaking, so the effect's else branch never ran for an unmount and the
three Animated.loop chains kept going. On web RNW's Animated uses the
JS driver: each loop is a requestAnimationFrame chain that restarts
forever (iterations: -1), leaking three 60 fps loops, their closures
and Animated.Values per speech turn for the life of the tab. Stop the
bars in the effect cleanup; the else branch stays for the mounted case.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Every native ShimmerText ran its own infinite withRepeat loop, one per
visible thinking row, for as long as the row stayed on screen. The
progress value now lives in a module-level makeMutable shared by all
instances: the first mount starts the loop, the last unmount cancels
it, each useAnimatedStyle just reads the shared value. Web is untouched
(Metro resolves ShimmerText.web.tsx there, which is a pure CSS shimmer).

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

This branch has not been deployed

No deployments
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