Dart 7 · Performance
React Native performance · completed audit

Less work between intent and response.

A release-mode, whole-app performance pass across startup, bundle composition, image memory, SQLite, Firebase sync, lists, replay, animations, and lifecycle cleanup. Changes were kept only when they solved a measured user-facing problem.

Baseline 5de9aa513fd7f658751f09caafcbaadeb398897a Final implementation cff750d9960d13da8a37de16ce800217439d0330 Generated 12 July 2026 at 15:09
−46.8%production export footprint108.7 MB → 57.9 MB
−4.7%Hermes bundle bytes17.17 MB → 16.36 MB
29distinct completed improvementsNo shared fix counted twice
41audited candidates7 partial · 4 deferred · 1 rejected
Measured outcome

Release-mode proof

The baseline and final exports use the same iOS production/Hermes/Atlas path. Payload and graph reductions are directly comparable; cold export time is reported honestly as build noise, not user startup speed.

Export footprintOTA/export payload
-46.8%
Baseline108.7 MB
Final57.9 MB
Bundled asset bytesDecoded separately at runtime
-53.2%
Baseline88.5 MB
Final41.4 MB
Hermes bytecodeProduction iOS bundle
-4.7%
Baseline17.2 MB
Final16.4 MB
Evaluated modulesAtlas production graph
-7.3%
Baseline5,238
Final4,855
Measurement note: final cold export wall time was 32.3 s versus 30.6 s at baseline (+5.6%). That metric measures local export work, not device responsiveness, so no faster-build claim is made.

Startup dependency closures

Narrow public subpaths stop unrelated domain data from evaluating before first content.

ClosureBaselineFinalBytes
Core root1,291 modules · 11.95 MBAbsentEliminated
Firebase client1,451 · 12.74 MB1,136 · 5.17 MB−59.4%
Local store1,333 · 12.24 MB998 · 4.48 MB−63.4%
Sync provider1,784 · 14.05 MB1,484 · 6.60 MB−53.0%
App providers2,226 · 16.93 MB1,926 · 9.48 MB−44.0%
Root layout2,718 · 18.56 MB2,639 · 16.67 MB−10.1%

History query scaling

Bulk summaries replace per-match query fan-out.

MatchesBeforeFinal
1075 queries10 queries
100705 queries10 queries
5003,505 queries11 queries
Screen by screen

Where users feel it

The work spans visible screens and their shared infrastructure. Each group links to its exact candidate records, including work deliberately not shipped.

3/6 completed

Home, matches & history

The scoring path now persists the minimum state, keeps live summaries scoped to the active match, and reads history in bounded bulk operations.

5/6 completed

Players, friends & avatars

Player-heavy screens avoid duplicate listeners and N+1 lookups; avatar upload, resolution, caching, decoding, and recycling have bounded memory behavior.

3/3 completed

Career & tournaments

Career state is local-first with shared revalidation, long rankings are virtualized, and tournament brackets render only the relevant work first.

4/4 completed

DartBot TV, voice & achievements

High-frequency replay progress moved off React state, hidden voice diagnostics stop subscribing, and celebration motion uses native execution with cleanup.

4/6 completed

Shared storage, sync & lifecycle

Scoped invalidation, SQL-only dirty selection, coalesced sync scheduling, indexes, and metadata-only listener fast paths reduce repeated work as accounts grow.

Visual evidence

Quality held, weight removed

Static comparisons demonstrate asset fidelity; runtime captures demonstrate that high-risk flows still render and respond after the internal changes. Invisible performance work is paired with numeric and automated evidence elsewhere in the report.

BeforeAchievement artwork · baseline · 523.3 KB
Achievement artwork · baseline
AfterAchievement artwork · optimized · 523.2 KB
Achievement artwork · optimized
BeforeFallback avatar · baseline · 1.6 MB
Fallback avatar · baseline
AfterFallback avatar · optimized · 239.6 KB
Fallback avatar · optimized
Runtime proofAndroid · home · 694.1 KB
Android · home
Runtime proofAndroid · dark appearance · 756.6 KB
Android · dark appearance
Runtime proofAndroid · settings · 222.6 KB
Android · settings
Runtime proofAndroid · completed match summary · 355.5 KB
Android · completed match summary
Runtime proofAndroid · DartBot TV match · 366.1 KB
Android · DartBot TV match
Runtime proofAndroid · rapid repeated scoring · 366.1 KB
Android · rapid repeated scoring
Runtime proofAndroid · reduced animation scale · 543.5 KB
Android · reduced animation scale
Runtime proofAndroid · permissions revoked · 358.1 KB
Android · permissions revoked
Runtime proofiOS · DevRelease launch · 1.2 MB
iOS · DevRelease launch
Runtime proofiOS · Reduce Motion enabled · 1.1 MB
iOS · Reduce Motion enabled
Rapid scoring interruption testShort runtime capture used for interruption and responsiveness checks.
Regression proof

Built, run, interrupted

Validation combines deterministic tests with release-mode export analysis and native simulator/emulator exercises. Environmental limitations are called out rather than converted into passes.

Passed

Complete repository validation

Zero lint errors, all strict workspace and backend typechecks passed, 768/768 DartBot bands passed, and every executed core/mobile test passed. The existing no-emulator guard skipped 11 Firestore rules cases.

Passed

iOS DevRelease build and launch

Built, installed, and launched on an iPhone 16e simulator. The launch-window crash scan found no fatal, crash, or exception record.

Passed

Android native build

The arm64 development APK built in 18s and installed successfully; SHA-256 a0b5c0a93c05…. Temporary native/Firebase prebuild output was removed afterward.

Passed

Rapid repeated interactions

Repeated 20/19/18 scoring and player handoffs remained serialized, interactive, and free of invalid intermediate controls or application crashes.

Passed

Accessibility and appearance states

Dark appearance, iOS Reduce Motion, Android zero animation scale, and match launch with camera and microphone permissions revoked all remained understandable and usable.

Passed with harness notes

Major seeded navigation flows

Focused home→players→settings, completed-summary, match-ready, DartBot TV, and onboarding checks passed. The full Maestro batch was limited by clearState forgetting Metro and the floating Expo Tools control intercepting top-right actions.

Diagnostic only

Frame timing captured, no false claim

Android gfxinfo was collected, but the development launcher, emulator GPU, UI automation, and simultaneous screen recording contaminated the sample. The data is retained in the evidence register; no release FPS improvement is claimed.

Explicitly deferred

Risky optimizations stayed out

Experimental Expo tree shaking reduced raw JavaScript 5.05% but increased build time about 2.8× and lacked full ordering coverage, so it was not shipped.

Complete record

Every candidate, one count

Search or filter the 41 distinct candidates. Completed items include the original problem, shipped change, user benefit, implementation location, verification, accessibility behavior, and reduced-motion behavior.

PERF-001

Performance measurement tooling

completed
Original problem

The repository has no repeatable production bundle measurement command or checked-in baseline schema, making bundle regressions difficult to detect.

What changed

Installed Expo Atlas as mobile development tooling, added repeatable production export and asset-budget commands, emitted performance-report.json, and retained this candidate register.

User benefit

Future changes are less likely to silently increase startup work, download size, or install size.

How it was verified

The scripted final iOS production/Hermes export completed and wrote the canonical metrics recorded in docs/validation/evidence/mobile-performance-final.json: 4,855 modules and a 16,364,336-byte bundle.

Implementation, accessibility & regression notes
Files
  • package.json
  • package-lock.json
  • apps/mobile/package.json
  • scripts/mobile-performance-export.mjs
  • scripts/check-mobile-asset-budgets.mjs
  • docs/validation/mobile-performance-register.json
Evidence
  • scripts/mobile-performance-export.mjs
  • scripts/check-mobile-asset-budgets.mjs
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

None directly; protects overall responsiveness for users of assistive technology as well as touch users. Verified behavior remained unchanged in the relevant automated/source checks.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Tooling is development-only and the production application runtime is unchanged; the report schema was parsed successfully with jq.

PERF-002

Achievement imagery

completed
Original problem

Achievement artwork is encoded at 1254 by 1254 pixels even though its largest in-app presentation is much smaller, inflating bundled bytes and decoded texture memory.

What changed

Resized and recompressed all 115 achievement WebP assets from 1254px sources to 512x512 display-appropriate images while preserving alpha.

User benefit

Faster installs and updates, lower memory pressure, and fewer image-decoding stalls on achievement-heavy screens.

How it was verified

The asset-budget check passes for all 115 files: 9,479,352 total bytes, 111,982-byte largest file, and 512x512 maximum dimensions. The final exported asset payload fell from 88,506,690 to 41,402,730 bytes across all asset optimizations.

Implementation, accessibility & regression notes
Files
  • apps/mobile/assets/achievements/images/*.webp
  • scripts/check-mobile-asset-budgets.mjs
Evidence
  • apps/mobile/assets/achievements/images
  • scripts/check-mobile-asset-budgets.mjs
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

Badge silhouettes, transparent edges, and contrast remain legible at the application's maximum rendered size.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Representative simple and detailed badges were inspected after recompression; no source dimensions exceed the verified budget.

PERF-003

Fallback avatar imagery

completed
Original problem

The light and dark unknown-player avatars are 1254-square PNGs above 1.4 MB each despite appearing as small avatars.

What changed

Resized both light and dark unknown-player fallback PNGs to 512x512 and recompressed them to approximately 224KB and 240KB.

User benefit

Lower app/update size and reduced peak decode memory in lists and profiles.

How it was verified

The asset-budget check passes for both 512x512 files: 463,906 total bytes and a 239,605-byte largest file; the final production export includes the reduced assets.

Implementation, accessibility & regression notes
Files
  • apps/mobile/assets/dart-home/unknown-avatars/unknown-player.png
  • apps/mobile/assets/dart-home/unknown-avatars/unknown-player-light.png
  • scripts/check-mobile-asset-budgets.mjs
Evidence
  • apps/mobile/assets/dart-home/unknown-avatars
  • scripts/check-mobile-asset-budgets.mjs
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

The fallback silhouette remains distinct in both light and dark themes.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Both theme variants were retained with stable paths and no consumer API change.

PERF-004

Production startup

completed
Original problem

The root layout statically imports and mounts the development E2E harness in every application variant.

What changed

Replaced the root's static E2E harness import with a tiny variant-aware host that dynamically imports seeded tournament/career/DartBot code only in development or debug variants; the host buffers initial and incoming URLs while the chunk loads.

User benefit

Less JavaScript module evaluation and less startup work before the first usable screen.

How it was verified

The standard production Atlas graph excludes the E2E harness feature closure. Warm seeded links passed in the Android development client and the deferred harness loaded as its own Metro chunk. A standalone DevRelease cold seeded-link routing limitation is documented in the native evidence; normal customer cold launch passed.

Implementation, accessibility & regression notes
Files
  • apps/mobile/app/_layout.tsx
  • apps/mobile/src/testing/e2e-harness-host.tsx
  • apps/mobile/src/testing/e2e-harness.tsx
  • apps/mobile/app.config.js
Evidence
  • apps/mobile/src/testing/e2e-harness-host.tsx
  • apps/mobile/app/_layout.tsx
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

Product behavior is unchanged; seeded development links remain available for accessibility QA.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Production selection uses the configured native app variant; Metro-backed clients additionally use __DEV__ because Constants can expose the native variant after root mount. Production exports exclude the harness, and pending URLs are buffered until the development chunk is ready.

PERF-005

Production startup

completed
Original problem

The root layout unconditionally imports expo-dev-client, including production export analysis.

What changed

Removed the unconditional expo-dev-client JavaScript side-effect import from the root layout while retaining the native expo-dev-client config plugin for development builds.

User benefit

Potentially less production startup evaluation and a smaller dependency surface.

How it was verified

Source audit finds no mobile runtime import of expo-dev-client and the production Atlas payload contains zero expo-dev-client modules.

Implementation, accessibility & regression notes
Files
  • apps/mobile/app/_layout.tsx
  • apps/mobile/app.config.js
Evidence
  • apps/mobile/app/_layout.tsx
  • apps/mobile/app.config.js
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

None. Verified behavior remained unchanged in the relevant automated/source checks.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

The native development-client plugin remains configured, so removing the redundant JavaScript import does not remove development launcher support.

PERF-006

Startup preferences

completed
Original problem

Theme and analytics providers gate their children on separate SecureStore reads in nested order, creating an avoidable serial startup waterfall.

What changed

Made analytics preference hydration non-blocking so the application subtree no longer waits behind a second SecureStore read; theme remains the sole visual gate to prevent a theme flash.

User benefit

The first screen becomes available sooner without a second storage round trip on the critical path.

How it was verified

Provider policy tests and the full mobile suite pass; analytics renders children immediately and never defaults an unhydrated preference to active capture.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/providers/app-providers.tsx
  • apps/mobile/src/providers/theme-provider.tsx
  • apps/mobile/src/providers/analytics-provider.tsx
  • apps/mobile/src/providers/analytics-runtime-policy.ts
Evidence
  • apps/mobile/src/providers/analytics-provider.tsx
  • apps/mobile/src/providers/analytics-runtime-policy.test.ts
  • apps/mobile/src/providers/theme-provider.tsx
Accessibility

No additional spinner or provider remount was introduced, and the visual theme still resolves before focusable content appears.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Consent remains fail-closed for capture while the serialized preference state loads.

PERF-007

Local state subscriptions

completed
Original problem

One untyped global store-change event forces every mounted local-store hook to requery after unrelated writes.

What changed

Introduced typed local-store invalidation topics and routed each hook/provider subscription only to the records it reads, retaining an explicit all-scope fallback.

User benefit

Fewer redundant SQLite queries and React updates during scoring, sync, settings changes, and background writes.

How it was verified

Focused invalidation tests cover scoped delivery, batched all-scope events, and isolated sync metadata; mobile typecheck and the full state/mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-store-invalidation.ts
  • apps/mobile/src/state/local-store-invalidation.test.ts
  • apps/mobile/src/providers/sync-provider.tsx
Evidence
  • apps/mobile/src/state/local-store-invalidation.ts
  • apps/mobile/src/state/local-store-invalidation.test.ts
  • apps/mobile/src/state/local-store.ts
Accessibility

Visible and announced state remains immediate for the subscribed entity while unrelated writes no longer trigger re-queries.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

High fan-out correctness risk is covered by topic-routing tests plus the all-topic escape hatch.

PERF-008

Match scoring persistence

completed
Original problem

Each saved dart triggers a full player aggregate and achievement rebuild across historical contribution rows and also scans live matches.

What changed

Separated snapshot persistence from completed-history aggregation so ordinary in-progress dart/score/undo writes persist game state without rebuilding lifetime statistics and achievements; terminal and completed-correction boundaries still rebuild.

User benefit

Lower input latency and fewer frame drops during the app's core scoring interaction, especially with long histories.

How it was verified

Policy tests prove ordinary scoring mutations perform snapshot-only work and terminal/correction paths receive the unchanged aggregate evidence; core/state/mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-match-persistence-policy.ts
  • apps/mobile/src/state/local-match-persistence-policy.test.ts
Evidence
  • apps/mobile/src/state/local-match-persistence-policy.ts
  • apps/mobile/src/state/local-match-persistence-policy.test.ts
  • apps/mobile/src/state/local-store.ts
Accessibility

Live score state is still persisted and announced immediately; only historical aggregate recomputation moved off the hot path.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Terminal and correction parity tests mitigate the high risk of stale lifetime statistics or achievement state.

PERF-009

Sync payload queries

completed
Original problem

Dirty-sync payload generation lists and parses every match and player before filtering dirty records in JavaScript.

What changed

Added SQL read models for dirty players, matches, counts, detachments, overrides, statistics, achievements, and trends so sync parses only rows it uploads.

User benefit

Background sync consumes less CPU, memory, and battery as match history grows.

How it was verified

SQLite fixtures verify dirty/clean mixtures, tournament-simulation exclusion, payload counts, unchanged-write guards, and indexed query plans; state/mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-db.ts
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-sync-queries.ts
  • apps/mobile/src/state/local-sync-queries.test.ts
Evidence
  • apps/mobile/src/state/local-sync-queries.ts
  • apps/mobile/src/state/local-sync-queries.test.ts
  • apps/mobile/src/state/local-db.ts
Accessibility

None directly; background work should not delay assistive interactions. Verified behavior remained unchanged in the relevant automated/source checks.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Payload equality and stale-write protection are covered by deterministic SQL fixtures.

PERF-010

Sync scheduling

completed
Original problem

A store change causes SyncProvider to refresh counters and then build a dirty payload, duplicating overlapping full-table scans.

What changed

Replaced duplicate global-change scans with a scoped sync topic, cheap dirty counts, a 50ms single-flight inspection queue, and one delayed sync timer for bursts.

User benefit

Less repeated background work during rapid scoring and batch imports.

How it was verified

Rapid local-change, metadata-only snapshot, offline, retry, and cancellation policies pass in backend/provider/state tests and the full mobile suite.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/providers/sync-provider.tsx
  • apps/mobile/src/state/local-db.ts
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-store-invalidation.ts
Evidence
  • apps/mobile/src/providers/sync-provider.tsx
  • apps/mobile/src/state/local-store-invalidation.test.ts
  • apps/mobile/src/backend/sync-snapshot-policy.test.ts
Accessibility

Sync status remains accurate while coalescing prevents repeated status churn from the same mutation burst.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Queued inspection and unmount cleanup prevent overlapping work or stale status commits.

PERF-011

Match history summaries

completed
Original problem

Match, player, and DartBot history screens call getLocalMatchSummary once per match with unbounded Promise.all; each summary issues additional per-player history queries.

What changed

Added a bounded bulk local-match overview projection and replaced per-match unbounded Promise.all summary/history fan-out across Matches, Home, notifications, player profiles, and DartBot profiles.

User benefit

History and profile screens open and scroll faster with large match libraries, without memory spikes.

How it was verified

Focused tests cover stable ordering, deduplication, single-pass parsing, and bounded reads for 10/100/500 matches. Estimated normal two-player query shape improves from 5+7N to 9+ceil(N/400): 75 to 10 at 10 matches, 705 to 10 at 100, and 3,505 to 11 at 500.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-match-overview.ts
  • apps/mobile/src/state/local-match-overview.test.ts
  • apps/mobile/src/state/local-db.ts
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/features/matches/matches-tab-body.tsx
  • apps/mobile/src/features/home/home-dashboard.tsx
  • apps/mobile/src/features/home/home-notifications-screen.tsx
  • apps/mobile/src/features/dartbot/dartbot-profile-screen.tsx
  • apps/mobile/app/(app)/(tabs)/players/[id].tsx
Evidence
  • apps/mobile/src/state/local-match-overview.ts
  • apps/mobile/src/state/local-match-overview.test.ts
  • apps/mobile/src/features/matches/matches-tab-body.tsx
Accessibility

Stable source ordering and keys preserve list reading order while loading semantics remain unchanged.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Projection parity tests and selective leg-detail retention protect summary correctness.

PERF-012

Firebase match listener

partial
Original problem

The cloud sync listener subscribes to the entire unbounded match history and reapplies full local aggregate reconciliation on listener or metadata changes.

Candidate evaluated

Process document changes incrementally, avoid metadata-only reapplication, and define a safe initial-history pagination strategy.

User benefit

Lower network, CPU, and battery use for established accounts and faster foreground resumption.

How it was verified

Test add/modify/remove, offline cache, metadata-only snapshots, account switches, reconnects, and old-history retrieval.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/state/local-store.ts
Evidence
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/backend/sync-snapshot-policy.ts
  • apps/mobile/src/backend/sync-snapshot-policy.test.ts
Accessibility

Remote changes must remain complete and deterministic; no stale screen-reader state.

Reduced motion

Not applicable; this candidate changes background data processing.

Risk / regression outcome

high

Status rationale

Metadata-only Firestore callbacks now skip local snapshot reconciliation and real add/modify/remove revisions are distinguished, but the initial listener still subscribes to the full match history and safe old-history pagination was not implemented.

Remaining work

Define and test bounded initial-history pagination plus incremental local reconciliation for established accounts, including offline cache, reconnect, account switch, and historical retrieval.

PERF-013

Career persistence

completed
Original problem

Every career screen mount attempts a cloud restore and then can upload unchanged local career state.

What changed

Added per-account/profile/generation single-flight career revalidation with a freshness window, explicit reload bypass, restore decision policy, and unchanged-write suppression.

User benefit

Career navigation performs fewer network and serialization operations and uses less battery.

How it was verified

Career tests cover same-profile sharing, account-switch generations, explicit reload, cloud failure retry, local/remote lineage decisions, and zero echo writes; 51 career tests and the mobile suite pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/career/use-career.ts
  • apps/mobile/src/features/career/career-service.ts
  • apps/mobile/src/features/career/career-revalidation-cache.ts
  • apps/mobile/src/features/career/career-revalidation-cache.test.ts
  • apps/mobile/src/features/career/career-restore-policy.ts
  • apps/mobile/src/features/career/career-restore-policy.test.ts
Evidence
  • apps/mobile/src/features/career/career-revalidation-cache.test.ts
  • apps/mobile/src/features/career/career-restore-policy.test.ts
  • apps/mobile/src/features/career/career-service.ts
Accessibility

Loading and errors remain profile-scoped and no stale result can expose another account's career.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Generation checks and failure eviction prevent stale-account reuse or permanently poisoned cache entries.

PERF-014

Relationship subscriptions

completed
Original problem

Friend search starts a second relationship subscription and relationship updates discard listener data before issuing profile/player N+1 reads.

What changed

Projected friend relationships directly from coordinated profile/player snapshot listeners and removed duplicate relationship/N+1 fetch behavior from friend search and related consumers.

User benefit

Friend and search screens react faster with fewer listeners and network round trips.

How it was verified

Backend tests cover listener readiness, selected main-player snapshots, request rows without private player reads, stable order, and avatar projection; the backend suite passes 11/11.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/backend/friend-relationship-projection.ts
  • apps/mobile/src/backend/friend-relationship-projection.test.ts
  • apps/mobile/src/features/players/player-add-data.ts
  • apps/mobile/app/(app)/(tabs)/players/new.tsx
Evidence
  • apps/mobile/src/backend/friend-relationship-projection.ts
  • apps/mobile/src/backend/friend-relationship-projection.test.ts
  • apps/mobile/src/backend/firebase-client.ts
Accessibility

Relationship state and stable ordering remain accurate for screen-reader announcements and focus.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Readiness gating prevents publishing incomplete friend rows while listeners converge.

PERF-015

Long lists

partial
Original problem

Several potentially unbounded data collections are rendered with ScrollView or with FlatList defaults that do not reflect row size and viewport needs.

Candidate evaluated

Virtualize genuinely unbounded collections and tune initialNumToRender, windowSize, batch size, clipping, stable keys, and layout hints only where measurement supports it.

User benefit

Lower mount time and memory use, with smoother scrolling on large datasets.

How it was verified

Profile representative small and large datasets, rapid scroll, focus traversal, empty states, and dynamic insertion/removal.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features
  • apps/mobile/app/(app)
Evidence
  • apps/mobile/src/features/matches/matches-tab-body.tsx
  • apps/mobile/src/features/career/career-secondary-screens.tsx
  • apps/mobile/src/features/players/players-rankings-screen.tsx
Accessibility

Preserve logical focus order, reachable content, and announcements as rows recycle.

Reduced motion

List recycling itself has no motion; any remaining row transitions must continue to honor the shared motion preference.

Risk / regression outcome

high

Status rationale

High-impact lists in Matches, Order of Merit, Tournament Bracket, and Player Rankings were virtualized and tuned under their distinct candidate IDs, but the complete app-wide inventory of potentially unbounded ScrollViews was not profiled or converted.

Remaining work

Profile remaining notification, catalog, tournament, and profile collections with large seeded datasets and verify focus traversal before any additional conversions.

PERF-016

List row rendering

partial
Original problem

Expensive history and catalog rows may receive unstable callbacks or recompute derived data on every parent update.

Candidate evaluated

Use stable renderItem/keyExtractor and focused memoization only on measured hot rows not already optimized by the compiler.

User benefit

Reduced JS work during scrolling and unrelated state changes.

How it was verified

Compare Profiler commit counts and duration before and after each focused change.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features
  • apps/mobile/app/(app)
Evidence
  • apps/mobile/src/features/players/players-rankings-components.tsx
  • apps/mobile/src/features/matches/matches-tab-body.tsx
Accessibility

Rows must still update immediately when labels, state, or disabled semantics change.

Reduced motion

Not applicable to row identity/memoization.

Risk / regression outcome

medium

Status rationale

Several hot list rows gained stable keys/callbacks and React Compiler remains enabled, but there is no complete React Profiler comparison proving which remaining rows benefit from manual memoization.

Remaining work

Capture release-mode Profiler commits for representative large histories/catalogs and add focused memoization only where compiler output does not already eliminate the work.

PERF-017

Expo Image usage

partial
Original problem

List imagery is not consistently given cache policy, priority, transition, or recycling identity appropriate to its role.

Candidate evaluated

Add role-based image defaults and list-specific recycling keys/priority where reused views can otherwise show stale or delayed content.

User benefit

Fewer decode/fetch stalls and less incorrect-image flashing during fast scrolls.

How it was verified

Test cold/warm cache, offline mode, rapid list recycling, theme changes, and replaced profile images.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/components
  • apps/mobile/src/features
Evidence
  • apps/mobile/src/components/player-avatar.tsx
Accessibility

Alternative labels and decorative-image behavior must remain correct.

Reduced motion

Any future image transition must be disabled or immediate under reduced motion.

Risk / regression outcome

medium

Status rationale

PlayerAvatar now has memory-disk caching and a recycling key, completing the remote-avatar-specific work in PERF-039, but role-based defaults were not verified across every Expo Image call site.

Remaining work

Audit cold/warm/offline behavior, priority, transitions, and recycling identity for achievement, tournament, profile, and decorative image roles.

PERF-018

Animation runtime

partial
Original problem

Layout-property animations, JavaScript-thread shared-value reads, or excessive simultaneously animated nodes can reduce frame throughput.

Candidate evaluated

Move measured hotspots to transform/opacity, remove JS-thread shared-value polling, and cap offscreen or repeated animation work.

User benefit

More stable gestures, navigation, and scrolling without changing the established motion language.

How it was verified

Inspect interruption behavior and release-mode frame traces with motion enabled and reduced motion enabled.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/components
  • apps/mobile/src/features
  • apps/mobile/src/lib/motion.ts
Evidence
  • apps/mobile/src/features/achievements/achievement-celebration.tsx
  • apps/mobile/src/features/dartbot-tv/replay-clock.ts
  • apps/mobile/src/providers/motion-provider.tsx
Accessibility

Reduced-motion behavior must remain immediate and equivalent.

Reduced motion

All landed hotspot fixes retain immediate final states; the remaining audit must verify every other motion path.

Risk / regression outcome

medium

Status rationale

Specific measured hotspots moved to native/UI-thread or bounded animation paths in PERF-027 through PERF-030, and ongoing indicators pause while inactive/reduced, but an app-wide release frame trace and complete layout-animation audit remain outstanding.

Remaining work

Capture release-mode frame/ETTrace evidence for navigation, long lists, gestures, and interruption; then address only measured remaining layout/shared-value hotspots.

PERF-019

Static career data

deferred
Original problem

The production module graph contains a 1.2 MB canonical career starting-state JSON module.

Candidate evaluated

Measure parse/evaluation cost and determine whether a compact generated representation or deferred evaluation can reduce startup and bundle cost without harming deterministic career state.

User benefit

Potentially faster startup and smaller downloads, especially for users who never enter Career.

How it was verified

Byte-compare generated data, run all career determinism tests, and measure release bundle and evaluation timing.

Implementation, accessibility & regression notes
Files
  • packages/core/src/domain/career/starting-state/canonical-v1.json
  • packages/core/src/domain/career-starting-state.ts
Evidence
  • packages/core/src/domain/career/starting-state/canonical-v1.json
  • packages/core/src/domain/career-starting-state.ts
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

None.

Reduced motion

Not applicable; static data representation only.

Risk / regression outcome

high

Status rationale

Narrow core imports remove canonical-v1.json from the startup provider closure, but the 1.2MB deterministic artifact remains in the production route graph and was not reformatted.

Remaining work

Design a byte-stable compact representation or route-level deferred loader, regenerate the artifact, run every career determinism/migration test, and compare parse/evaluation timing.

PERF-020

Static DartBot data

deferred
Original problem

The production module graph contains a generated DartBot player pool above 1 MB plus additional calibration modules.

Candidate evaluated

Measure access patterns and evaluate a compact generated representation, lookup partitioning, or deferred construction while preserving deterministic bot selection.

User benefit

Potentially lower bundle parse cost and faster non-DartBot startup.

How it was verified

Run the full DartBot and calibration suites, compare generated records, and remeasure production export.

Implementation, accessibility & regression notes
Files
  • packages/core/src/domain/dartbot/generated-player-pool.ts
  • packages/core/src/domain/dartbot/profile-registry.ts
  • packages/core/src/domain/dartbot/catalog.ts
Evidence
  • packages/core/src/domain/dartbot/generated-player-pool.ts
  • packages/core/src/domain/dartbot/profile-registry.ts
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

None.

Reduced motion

Not applicable; static data representation only.

Risk / regression outcome

high

Status rationale

Narrow core imports remove the generated player pool from AppProviders/startup, but the pool and calibration data remain required by DartBot routes and were not compacted or partitioned.

Remaining work

Measure DartBot route access/parse cost, prototype a compact or partitioned generated format, and run the complete generation, calibration, simulation, and determinism suites.

PERF-021

Observability bundle/runtime

deferred
Original problem

Sentry replay and feedback packages contribute substantial production module weight, and runtime integrations may impose work even when features are unused.

Candidate evaluated

Verify configured integrations and tree-shaking behavior; exclude unused replay/feedback functionality only if crash and performance monitoring remain intact.

User benefit

Potentially smaller download and less monitoring overhead without losing actionable crash diagnostics.

How it was verified

Compare Atlas package totals and send a controlled test error plus performance span in a release build.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/lib/sentry.ts
  • apps/mobile/app/_layout.tsx
  • apps/mobile/metro.config.js
Evidence
  • apps/mobile/src/lib/sentry.ts
  • apps/mobile/app/_layout.tsx
Accessibility

None.

Reduced motion

Not applicable; observability integration selection only.

Risk / regression outcome

high

Status rationale

Sentry crash monitoring remained intact, but replay/feedback integrations were not removed because release error/span verification was unavailable and a blind import change could reduce diagnostic coverage.

Remaining work

Measure configured release integrations, send a controlled crash and performance span, and exclude replay/feedback only with proof that required diagnostics remain.

PERF-022

Production logging

rejected
Original problem

Console calls in production can block the JavaScript thread if application or dependency logs reach hot paths.

Candidate evaluated

Confirm hot-path logging behavior and configure production console removal only if it does not hide Sentry breadcrumbs or actionable errors.

User benefit

Avoidable logging cannot cause scoring or navigation stalls.

How it was verified

Inspect release output and confirm error reporting remains operational.

Implementation, accessibility & regression notes
Files
  • apps/mobile/metro.config.js
  • apps/mobile/src/lib/perf-timing.ts
Evidence
  • apps/mobile/src/lib/perf-timing.ts
  • apps/mobile/src/lib/sentry.ts
Accessibility

None.

Reduced motion

Not applicable; logging only.

Risk / regression outcome

medium

Status rationale

Rejected after source and release-output inspection found limited application logging, with performance timing already gated; blanket console removal had no demonstrated hot-path benefit and risked hiding Sentry breadcrumbs or actionable errors.

Remaining work

None for this pull request. Reopen only if a release trace identifies a concrete production logging hotspot.

PERF-023

SQLite indexes and query plans

completed
Original problem

Growth-sensitive local queries may lack indexes for dirty state, player contribution history, status, and update ordering.

What changed

Added partial/composite indexes for dirty sync rows, match ordering, contribution history, and trend history, then committed EXPLAIN QUERY PLAN assertions for the hot reads.

User benefit

Local history, aggregate rebuild, and sync performance scales better as data grows.

How it was verified

SQLite query-plan tests assert the named dirty/history indexes are used and verify fresh-schema execution; the state and full mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-db.ts
  • apps/mobile/src/state/local-sync-queries.ts
  • apps/mobile/src/state/local-sync-queries.test.ts
Evidence
  • apps/mobile/src/state/local-sync-queries.ts
  • apps/mobile/src/state/local-sync-queries.test.ts
  • apps/mobile/src/state/local-db.ts
Accessibility

None directly. Verified behavior remained unchanged in the relevant automated/source checks.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Indexes are created idempotently with IF NOT EXISTS and query-plan fixtures guard against silent planner regressions.

PERF-024

Lifecycle cleanup and background work

partial
Original problem

Timers, app-state handlers, listeners, and async effects across providers and screens can continue work after blur or overlap after rapid remounts.

Candidate evaluated

Cancel superseded work, unsubscribe on blur/unmount, and coalesce bursts at lifecycle boundaries.

User benefit

Lower battery usage, fewer stale updates, and more responsive rapid navigation.

How it was verified

Rapidly navigate, background/foreground, switch accounts, cancel operations, and inspect listener/timer counts.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/providers
  • apps/mobile/src/features
  • apps/mobile/app
Evidence
  • apps/mobile/src/providers/sync-provider.tsx
  • apps/mobile/src/providers/motion-provider.tsx
  • apps/mobile/src/providers/toast-provider.tsx
  • apps/mobile/src/features/dartbot-tv/replay-clock.ts
Accessibility

Necessary status updates must not be cancelled before assistive users receive the final state.

Reduced motion

Landed ongoing/animation cleanup honors reduced motion; remaining lifecycle work must preserve final accessible state delivery.

Risk / regression outcome

medium

Status rationale

Concrete cleanup/coalescing fixes landed in sync, replay, toast, settings, and motion paths, but a complete app-wide listener/timer inventory with runtime counts was not finished.

Remaining work

Instrument listener/timer counts during rapid navigation, background/foreground, cancellation, and account switching, then fix only traced leaks or overlaps.

PERF-025

Live match rendering

completed
Original problem

The live match route mounts both the game-state loader and the heavyweight lifetime-aware match-summary loader, so every dart repeats historical contribution and achievement work.

What changed

The live route now consumes the match aggregate's lightweight liveSummary projection and reserves lifetime-aware completed summary/achievement history work for terminal confirmation paths.

User benefit

Lower dart-to-score latency and fewer dropped frames during long matches or on accounts with deep history.

How it was verified

Live and completed scoring/state tests pass, ordinary scoring persistence performs no contribution-history rebuild, and the live route renders its statistics from liveSummary.

Implementation, accessibility & regression notes
Files
  • apps/mobile/app/(app)/match/[id]/index.tsx
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-match-persistence-policy.ts
Evidence
  • apps/mobile/app/(app)/match/[id]/index.tsx
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-match-persistence-policy.test.ts
Accessibility

Visible statistics and score announcements still derive from the current persisted match state.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Completed/correction boundaries retain full history evidence, mitigating summary and adaptive-level regressions.

PERF-026

Voice debug rendering

completed
Original problem

The core voice-input hook subscribes React state to the debug log even when debug UI is hidden, and training ignores the returned log entirely.

What changed

Removed React debug-log subscription from the core voice input hook and mounted it only in the visible match debug panel; training no longer subscribes to unused log rows.

User benefit

Voice scoring no longer causes unrelated full-screen React commits for hidden diagnostic information.

How it was verified

Voice and training suites pass; source inspection confirms hidden debug collection no longer owns React state in the full input hook.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/voice/use-match-voice-input.ts
  • apps/mobile/src/features/voice/voice-debug-log.ts
  • apps/mobile/app/(app)/match/[id]/index.tsx
  • apps/mobile/src/features/training/training-screens.tsx
Evidence
  • apps/mobile/src/features/voice/use-match-voice-input.ts
  • apps/mobile/src/features/voice/voice-debug-log.ts
  • apps/mobile/app/(app)/match/[id]/index.tsx
Accessibility

Spoken scoring status and rejection feedback are unchanged; only hidden developer diagnostics stopped rerendering the screen.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Debug entries remain available when the explicit panel is enabled.

PERF-027

Achievement animation

completed
Original problem

Achievement celebration entrance, reveal, float, and confetti transform/opacity animations explicitly run on the JavaScript thread.

What changed

Moved all eligible achievement entrance, reveal, float, and confetti transform/opacity animations to the React Native native driver while retaining interruption cleanup.

User benefit

Achievement completion remains smooth when JavaScript is simultaneously persisting and synchronizing the completed match.

How it was verified

All four animation groups now use useNativeDriver: true; motion/core/mobile suites pass without unsupported-driver warnings.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/achievements/achievement-celebration.tsx
Evidence
  • apps/mobile/src/features/achievements/achievement-celebration.tsx
  • apps/mobile/src/lib/motion-spec.test.ts
Accessibility

Achievement content and completion announcements remain unchanged.

Reduced motion

Reduced-motion mode bypasses meaningful travel and exposes the completed celebration state immediately.

Risk / regression outcome

Only native-driver-supported transform and opacity properties were moved; cancellation paths remain.

PERF-028

Career Order of Merit

completed
Original problem

The ranking screen mounts and animates approximately 361 rows inside a ScrollView.

What changed

Converted the approximately 361-row Order of Merit from ScrollView mapping to a tuned FlatList and limited rank-transition animation to mounted rows with actual rank changes.

User benefit

Faster ranking-screen opening, bounded memory, and smooth access to low-ranked entries.

How it was verified

Career tests prove virtualized rows do not animate initial layout and mounted rows animate only real changes; rank 361 remains reachable with stable ordering.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/career/career-secondary-screens.tsx
  • apps/mobile/src/features/career/career-screen-shell.tsx
  • apps/mobile/src/features/career/career-rank-transition.tsx
  • apps/mobile/src/features/career/career-rank-transition-policy.test.ts
Evidence
  • apps/mobile/src/features/career/career-secondary-screens.tsx
  • apps/mobile/src/features/career/career-rank-transition-policy.test.ts
Accessibility

Stable keys and source order preserve rank reading and focus identity while rows recycle.

Reduced motion

Rank changes render their final position immediately when reduced motion is enabled.

Risk / regression outcome

Conservative FlatList batching and explicit policy tests cover recycling and rank-change behavior.

PERF-029

Tournament bracket

completed
Original problem

A 128-player bracket initially mounts the first two heavy round columns and only later imperatively scrolls to the desired round; every fixture receives entry/layout animation.

What changed

Added fixed getItemLayout, initialScrollIndex, one-column initial/batch rendering, a small window, and removed initial per-fixture layout animation for tournament brackets.

User benefit

Large brackets show the relevant round sooner with less layout and animation work.

How it was verified

Tournament motion tests cover active/completed initial round selection and layout fallback; the tournament and mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/tournaments/tournament-run-detail-screen.tsx
  • apps/mobile/src/features/tournaments/tournament-run-motion.ts
  • apps/mobile/src/features/tournaments/tournament-run-motion.test.ts
Evidence
  • apps/mobile/src/features/tournaments/tournament-run-detail-screen.tsx
  • apps/mobile/src/features/tournaments/tournament-run-motion.test.ts
Accessibility

The initially visible and announced round now matches the active/final round instead of imperatively jumping after mount.

Reduced motion

Initial bracket cards appear without decorative entry/layout motion; reduced-motion users see the target round immediately.

Risk / regression outcome

Fixed dimensions provide deterministic offsets and the existing failure fallback remains.

PERF-030

DartBot TV replay

completed
Original problem

Replay updates top-level React elapsed state every 32 ms, rerendering board, scoreboard, chrome, and controls while animating progress-bar width.

What changed

Replaced the 32ms top-level React elapsed-state loop with one UI-thread shared clock; React state advances only at discrete replay boundaries and progress uses transform-based animation.

User benefit

Replay remains smooth and controls stay responsive on slower devices.

How it was verified

Replay-clock tests cover presentation boundaries, pause/resume, completion, and monotonic elapsed projection; DartBot TV and mobile suites pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/dartbot-tv/career-broadcast-replay-screen.tsx
  • apps/mobile/src/features/dartbot-tv/replay-clock.ts
  • apps/mobile/src/features/dartbot-tv/replay-clock.test.ts
Evidence
  • apps/mobile/src/features/dartbot-tv/replay-clock.ts
  • apps/mobile/src/features/dartbot-tv/replay-clock.test.ts
  • apps/mobile/src/features/dartbot-tv/career-broadcast-replay-screen.tsx
Accessibility

Pause and replay position remain semantically current without announcing frame-level progress.

Reduced motion

Reduced motion preserves immediate discrete phase changes and avoids decorative continuous transitions while playback semantics remain usable.

Risk / regression outcome

A pure clock policy and boundary tests mitigate pause/background/interruption drift.

PERF-031

Icon asset bundle

completed
Original problem

Imports from the @expo/vector-icons root expose 19 icon font families even though the mobile app uses only Ionicons, MaterialCommunityIcons, and Feather.

What changed

Replaced @expo/vector-icons barrel imports with direct Ionicons, MaterialCommunityIcons, and Feather family modules and added an ESLint restriction against the root import.

User benefit

Smaller install/update payload without any visible icon change.

How it was verified

Full lint has zero errors, source audit finds no root icon import, and the final production export bundles only the three used font families.

Implementation, accessibility & regression notes
Files
  • apps/mobile/**/*.tsx
  • apps/mobile/eslint.config.mjs
Evidence
  • apps/mobile/eslint.config.mjs
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

Visible glyphs, labels, and accompanying accessible text are unchanged.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Typecheck and route smoke coverage guard family/name compatibility.

PERF-032

Unused UI runtime

completed
Original problem

ThemeProvider mounts RepoUIProvider/Tamagui even though the active mobile app does not render shared Tamagui components.

What changed

Exposed shared UI tokens through a narrow package subpath, imported those values directly, and removed the unused RepoUIProvider/Tamagui runtime wrapper from MobileThemeProvider.

User benefit

Less startup evaluation and a smaller JavaScript bundle with unchanged visual tokens.

How it was verified

Source audit finds no active Tamagui component consumer/provider; mobile typecheck, dark/light theme tests, and final Atlas export pass.

Implementation, accessibility & regression notes
Files
  • packages/ui/package.json
  • packages/ui/src/tokens.ts
  • apps/mobile/src/lib/theme.ts
  • apps/mobile/src/providers/theme-provider.tsx
Evidence
  • packages/ui/package.json
  • apps/mobile/src/providers/theme-provider.tsx
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

The same token values and system light/dark contrast behavior remain in use.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Only an unused runtime provider was removed; token resolution remains a supported package API.

PERF-033

Font startup

partial
Original problem

Navigation waits for runtime loading of five font files after persisted-provider gates, and two included font files are unused.

Candidate evaluated

Embed only the four used fonts with the expo-font config plugin and remove the runtime font gate.

User benefit

First application content can render without a separate JavaScript font-loading phase.

How it was verified

Build a fresh native app, inspect font availability on cold launch, and exercise English/German text at large sizes.

Implementation, accessibility & regression notes
Files
  • apps/mobile/app.config.js
  • apps/mobile/app/_layout.tsx
  • apps/mobile/src/lib/fonts.ts
Evidence
  • apps/mobile/app.config.js
  • apps/mobile/app/_layout.tsx
  • apps/mobile/src/lib/fonts.ts
Accessibility

No fallback-font flash or missing glyphs; Dynamic Type behavior remains unchanged.

Reduced motion

The current spinner is static; eventual gate removal must not introduce a font transition or layout shift.

Risk / regression outcome

medium

Status rationale

The expo-font config plugin now embeds only the four used Satoshi files and two unused font assets were removed from the load map, but the JavaScript useFonts spinner gate remains pending a fresh native-build verification of embedded family names.

Remaining work

Build a fresh iOS and Android development app, verify all four embedded families on cold launch and at large text sizes, then remove useFonts/AppBootstrap gating and the runtime asset map.

PERF-034

Core module evaluation

completed
Original problem

Startup consumers import @repo/core's root barrel, which eagerly reexports most domains and pulls career/DartBot generated data into the root evaluation closure.

What changed

Added supported narrow @repo/core exports, migrated every mobile runtime root-barrel import to subpaths across 48 core-consuming mobile files, split username helpers from Zod, and split pure career archive normalization from DartBot replay execution; ESLint now rejects runtime root imports while allowing type-only imports.

User benefit

Less JavaScript evaluation and memory allocation before the first screen.

How it was verified

Runtime @repo/core root imports are zero. Atlas root closure changed from 1,291 modules/11,951,072 transformed bytes to absent; AppProviders changed from 2,226/16,928,746 to 1,926/9,476,024 (-44.0%) and contains neither canonical career JSON nor the DartBot pool.

Implementation, accessibility & regression notes
Files
  • packages/core/package.json
  • packages/core/src
  • packages/core/src/domain/career/archive.ts
  • packages/core/src/domain/career/archive-replay.ts
  • apps/mobile/eslint.config.mjs
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/providers/sync-provider.tsx
  • apps/mobile/src/lib/sentry.ts
Evidence
  • packages/core/package.json
  • apps/mobile/eslint.config.mjs
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

This is an import/evaluation-boundary change; product semantics and accessible output remain unchanged.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Core/mobile typechecks, core 210/210, backend 11/11, career 51/51, affected feature suites, production export, and the lint boundary all pass.

PERF-035

Root component imports

completed
Original problem

The root layout imports AppStatusBar through a broad Midnight UI barrel, expanding its eager dependency closure.

What changed

Changed the root layout to import AppStatusBar directly from midnight-screen instead of the broad Midnight UI barrel.

User benefit

Less eager module evaluation during startup with identical status-bar behavior.

How it was verified

Mobile typecheck/lint pass and Atlas no longer reaches the barrel's approximately 168KB exclusive closure through AppStatusBar.

Implementation, accessibility & regression notes
Files
  • apps/mobile/app/_layout.tsx
  • apps/mobile/src/components/chrome/midnight-screen.tsx
Evidence
  • apps/mobile/app/_layout.tsx
  • apps/mobile/src/components/chrome/midnight-screen.tsx
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

Status-bar theme behavior is unchanged in light and dark mode.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

The defining component is imported directly without changing its API.

PERF-036

Local database startup

completed
Original problem

Concurrent consumers can race local-store initialization, while both local databases retry historical ALTER TABLE statements and catch duplicate-column errors on every launch.

What changed

Replaced boolean/racing initialization with shared retryable promises and added transactional PRAGMA user_version migrations for local and career databases, inspecting actual columns instead of relying on duplicate-column exceptions.

User benefit

Less duplicate startup work and deterministic database readiness on cold launch.

How it was verified

Migration tests cover fresh, original, partial, current-unversioned, failed-open retry, and future-version databases; legacy SQL smoke preserved rows, backfilled defaults, and stamped the expected version.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/state/local-store.ts
  • apps/mobile/src/state/local-db.ts
  • apps/mobile/src/state/local-db-migrations.ts
  • apps/mobile/src/state/local-db-migrations.test.ts
  • apps/mobile/src/features/career/career-repository.ts
  • apps/mobile/src/features/career/career-database-migrations.ts
  • apps/mobile/src/features/career/career-database-migrations.test.ts
Evidence
  • apps/mobile/src/state/local-db-migrations.test.ts
  • apps/mobile/src/features/career/career-database-migrations.test.ts
  • apps/mobile/src/features/career/career-repository.ts
Accessibility

Loading and recovery surfaces are unchanged; readiness is now deterministic.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

High schema risk is mitigated by transactional upgrades, column inspection, future-version non-downgrade, handle cleanup, and retry tests.

PERF-037

Avatar upload memory

completed
Original problem

Custom avatar upload reads the complete JPEG into a JavaScript base64 string before handing it back to the native Firebase Storage SDK.

What changed

Replaced FileSystem base64 loading plus Firebase putString with the native Storage putFile(localUri) upload path and removed the JavaScript-sized base64 allocation.

User benefit

Lower peak memory, less bridge work, and fewer upload stalls or memory-pressure failures on large camera/library images.

How it was verified

Mobile typecheck/backend tests pass and the upload retains content type, ownership metadata, bucket error translation, cache invalidation, and updatedAt behavior.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/features/players/player-avatar-image.ts
Evidence
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/features/players/player-avatar-image.ts
Accessibility

Upload progress/error presentation and avatar labels are unchanged.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

The native SDK's documented file path replaces only the transport representation; error handling remains intact.

PERF-038

Player rankings

completed
Original problem

Rankings performs per-player local/remote statistic reads, computes ranks with repeated array searches, and non-bot filters can mount every row in a ScrollView.

What changed

Added a lean remote leaderboard statistic read, computed rank maps in one pass, stabilized ranking derivation, and rendered both bot and non-bot filters with tuned accessible FlatLists.

User benefit

Faster rankings first paint and smooth scrolling as local players and friends grow.

How it was verified

Ranking tests verify stable ordering/rank output; 0/10/100/500 data paths, filters, remote errors, account changes, typecheck, lint, and full mobile tests pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/features/players/players-rankings-screen.tsx
  • apps/mobile/src/features/players/players-rankings-data.ts
  • apps/mobile/src/features/players/players-rankings-data.test.ts
  • apps/mobile/src/features/players/players-rankings-components.tsx
  • apps/mobile/src/backend/firebase-client.ts
Evidence
  • apps/mobile/src/features/players/players-rankings-data.test.ts
  • apps/mobile/src/features/players/players-rankings-screen.tsx
  • apps/mobile/src/backend/firebase-client.ts
Accessibility

Stable keys and rank order preserve focus identity and logical reading order while rows recycle.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Conservative list batching and deterministic rank tests protect filter and ordering parity.

PERF-039

Remote avatar resolution

completed
Original problem

Simultaneously mounted avatar rows can request the same Firebase Storage download URL more than once, and the resolved URL cache grows without a bound.

What changed

Added a 128-entry LRU promise cache keyed by avatar path/version, sharing in-flight URL loads, evicting failures, invalidating every version after upload/delete, and assigning Expo Image memory-disk cache policy plus recycling identity.

User benefit

Avatar-heavy rankings, history, and profile screens make fewer network calls, reuse decoded content reliably, and avoid unbounded JavaScript cache growth.

How it was verified

Four focused cache tests cover concurrent single-flight, resolved reuse, rejection retry, LRU eviction, and path-version invalidation; the combined backend suite passes 11/11 and mobile typecheck/lint pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/backend/bounded-async-cache.ts
  • apps/mobile/src/backend/bounded-async-cache.test.ts
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/components/player-avatar.tsx
  • apps/mobile/package.json
Evidence
  • apps/mobile/src/backend/bounded-async-cache.test.ts
  • apps/mobile/src/backend/firebase-client.ts
  • apps/mobile/src/components/player-avatar.tsx
Accessibility

Recycling identity follows the resolved player image/name and never changes the player's accessible label.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Failures cannot poison the cache, in-flight invalidation cannot reinsert stale values, and the size is strictly bounded.

PERF-040

Opt-in analytics startup

completed
Original problem

The application evaluates PostHog, touch-autocapture, and session-replay JavaScript at startup even when analytics is unconfigured, disabled, or the persisted preference has not loaded.

What changed

Removed static PostHog and PostHogProvider evaluation, rendered the product subtree immediately, and dynamically loaded one shared client only after configuration plus persisted opt-in; rapid preference and identity commands are serialized with stale-generation guards.

User benefit

First content appears with less JavaScript evaluation and no analytics-only loading gate; disabled analytics consumes no SDK runtime work.

How it was verified

Provider tests cover configuration, hydration, stale loads, unmount, and rapid toggles; production closure inspection shows asyncRequire only with no top-level PostHog require/provider wrapper; full mobile suite/typecheck/lint pass.

Implementation, accessibility & regression notes
Files
  • apps/mobile/src/providers/analytics-provider.tsx
  • apps/mobile/src/providers/analytics-runtime-policy.ts
  • apps/mobile/src/providers/analytics-runtime-policy.test.ts
  • apps/mobile/package.json
Evidence
  • apps/mobile/src/providers/analytics-provider.tsx
  • apps/mobile/src/providers/analytics-runtime-policy.test.ts
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

The provider never adds a spinner or remounts the product subtree, so delayed SDK loading cannot reset focus.

Reduced motion

Not applicable; this improvement does not add or alter motion.

Risk / regression outcome

Generation checks, serialized commands, fail-closed consent, and unmount tests mitigate stale SDK activation.

PERF-041

Experimental Expo graph optimization

deferred
Original problem

Standard production minification retains some unused cross-module exports that Expo's experimental whole-graph optimizer can remove.

Candidate evaluated

Evaluate Expo's experimental whole-graph optimization and tree-shaking flags, adopting them only after comparable native release coverage proves module order and side effects remain correct.

User benefit

Potentially smaller OTA downloads and less JavaScript to parse or store.

How it was verified

Compare production exports and then run the complete native/seeded suite with both experimental flags before adoption.

Implementation, accessibility & regression notes
Files
  • apps/mobile/metro.config.js
  • apps/mobile/.env.production.local.example
Evidence
  • apps/mobile/metro.config.js
  • docs/validation/evidence/mobile-performance-final.json
Accessibility

No intended UI change; a module-order regression could break any flow, including assistive behavior.

Reduced motion

No intended motion change; optimizer adoption still requires reduced-motion flow coverage.

Risk / regression outcome

high

Status rationale

The experiment demonstrated a 743,483-byte (5.05%) raw-JavaScript reduction, but the whole-graph optimizer is experimental, increased build time about 2.8x, and was not adopted without full native side-effect/module-order coverage.

Remaining work

Run comparable iOS/Android native, seeded navigation, auth, sync, audio, voice, and accessibility suites with both optimizer flags before considering production adoption.