Critical-journey performance acceptance β
Status: the capture and comparison harness is implemented and unit-tested. No React baseline or Flutter candidate report has been captured in this repository. A launch gate is not satisfied until the commands below run against the frozen React deployment and the release-candidate Flutter deployment.
The authoritative release run is the manual .github/workflows/performance-parity.yml workflow. It builds the pinned React commit and selected Flutter commit on one clean GitHub-hosted runner, refreshes staging-only sessions from encrypted inputs, enforces the gate, and retains the three JSON reports for 30 days. Local commands remain available for harness development and focused diagnosis, but a contended workstation is not release evidence.
What this gate measures β
tool/performance/journeys.json maps every critical journey named in the cross-platform launch plan to a source-backed route checkpoint. Each target is measured in Arabic and English, with the same Chromium build, viewport, CPU slowdown, network profile, immutable sanitized fixture set and logical test principal set.
Five isolated samples follow one discarded warm-up. The comparator uses the p75 of:
- first contentful paint (FCP)
- cumulative layout shift (CLS)
- total blocking time (TBT), derived from the blocking portion of browser long tasks after FCP through the explicit framework-ready signal
- browser-visible error rate (failed requests, HTTP 5xx responses, console errors and uncaught page errors divided by measured requests)
Flutter fails when any p75 is more than 10% worse than the paired React p75. Exactly 10% passes. A zero React baseline is strict: the Flutter result must also be zero. Missing metrics, capture errors, too few samples, different fixture URL hashes, different fixture/principal sets, different capture settings or different Chromium versions fail closed.
Each journey/locale/profile entry launches a fresh Chromium process. Its discarded warm-up and five isolated-context measurements share only that process, so the warm-up primes framework compilation without allowing Wasm or DevTools state to leak into another matrix entry. Reports record browserIsolation: "per-matrix-entry", and the comparator rejects any other mode.
The report also retains diagnostic route-ready time and LCP when the browser exposes them. They are not cross-renderer gates: Flutter canvas content does not produce a browser DOM LCP entry, and Flutter's first-frame event includes cold Wasm download/compilation while React's mount signal does not. Pre-FCP Wasm work remains visible in route-ready time but is not mislabeled as TBT. Chromium-cancelled resource selection requests (net::ERR_ABORTED, typically unused font variants) are not application errors.
The reports contain SHA-256 hashes of resolved fixture paths, not raw portal tokens or record IDs.
This is a lab comparison of production-like route checkpoints. It does not replace the write-path integration tests that validate each operation's database mutations, and it does not claim production field performance or native iOS/Android performance.
One-time runner setup β
Use a stable launch runner with Node.js 20 or newer:
Push-Location tool/performance
npm ci
npx playwright install chromium
npm test
Pop-LocationCopy tool/performance/fixtures.example.json to a private file under build/performance/ and replace every placeholder. The fixture file must identify one immutable sanitized fixture set in the dedicated staging Supabase project.
For authenticated checkpoints, create Playwright storage-state files in a private directory. The filenames are part of the harness contract:
react-brokerage_agent.json
react-valuer.json
react-accountant.json
react-admin.json
flutter-brokerage_agent.json
flutter-valuer.json
flutter-accountant.json
flutter-admin.jsonThe React and Flutter files may be origin-specific, but they must represent the same logical accounts identified by principalSetId. Do not commit fixture or storage-state files. The runner persists session refresh-token rotation back to these private files after each isolated sample, which keeps comprehensive captures valid when they exceed the staging JWT lifetime.
When the immutable sanitized fixtures already exist and only the expiring role sessions need regeneration, use the auth-only mode. It validates the fixture set, staging project and principal set before writing origin-scoped storage state, and it does not require or accept a service-role database write:
node tool/performance/prepare_staging.mjs `
--config env/mobile-e2e-staging.json `
--output-dir build/performance `
--existing-fixtures build/performance/fixtures.json `
--react-origin http://127.0.0.1:8081 `
--flutter-origin http://127.0.0.1:8090 `
--fixture-set-id plan1-20260729-rc9261bbc1 `
--principal-set-id mobile-e2e-roles-v1Capture the paired reports β
Use immutable deployment/build identifiers, not labels such as latest:
$reactUrl = 'https://REACT-FROZEN-STAGING.example'
$flutterUrl = 'https://FLUTTER-RC-STAGING.example'
$fixtureFile = 'build/performance/fixtures.json'
$authStateDir = 'build/performance/auth'
node tool/performance/capture.mjs `
--target react `
--base-url $reactUrl `
--build-id REACT_DEPLOYMENT_OR_COMMIT_SHA `
--fixtures $fixtureFile `
--auth-state-dir $authStateDir `
--output build/performance/react-baseline.json
node tool/performance/capture.mjs `
--target flutter `
--base-url $flutterUrl `
--build-id FLUTTER_DEPLOYMENT_OR_COMMIT_SHA `
--fixtures $fixtureFile `
--auth-state-dir $authStateDir `
--output build/performance/flutter-candidate.jsonThe full capture is intentionally comprehensive. For runner diagnosis only, --journeys, --locales, --profiles and --samples accept comma-separated filters. A filtered or undersampled report is not a full launch artifact.
Compare and persist the machine-readable gate:
node tool/performance/compare.mjs `
--baseline build/performance/react-baseline.json `
--candidate build/performance/flutter-candidate.json `
--output build/performance/performance-gate.jsonExit code 0 means all comparisons pass. Exit code 1 means a measured regression failed the gate. Exit code 2 means the inputs or capture contract are invalid.
External prerequisites still required β
The harness cannot manufacture these inputs:
- A live, immutable frozen-React staging URL and its deployment/commit ID.
- A live Flutter Web release-candidate staging URL and its deployment/commit ID.
- Both deployments configured to the same dedicated staging Supabase project, production schema and immutable sanitized fixture set.
- The fixture IDs and public portal tokens listed in
fixtures.example.json. - Origin-specific storage states for the same agent, valuer, accountant and administrator test principals.
- A stable runner with the Playwright-pinned Chromium installed, consistent network access, and no VPN/proxy changes between the paired captures.
- Separate real-device measurements on the supported iOS and Android release builds. A browser comparison to frozen React cannot establish native camera, biometric, notification, offline or device-rendering performance.
Until those inputs exist and the resulting gate artifact passes, the plan's performance acceptance requirement remains open.
