native-ios-perf-testing
Test native iOS app performance autonomously from the CLI — measure FPS during scroll/interaction, drive touch input, capture screenshots, stream runtime logs, and run before/after A/B comparisons of code changes. Use this whenever the user reports iOS UI jank, dropped frames, choppy scroll, animation hitches, "feels slow", layout jitter, or asks to validate that a perf change actually helped on iOS — *especially* in React Native / Expo apps. Also use when the user wants to drive the iOS Simulator from the command line (taps, swipes, scrolls), wants to verify a visual change without manual interaction, or asks how to test something "without me having to do it manually". Triggers include phrases like "60fps", "scroll feels janky", "test on simulator", "automate iOS testing", "FPS counter", "is my list smooth", "before/after on iOS", and references to xcrun, simctl, idb, or xctrace. Do NOT use for web apps (use a browser preview instead) or for unit/integration test runners — this skill is specifically for live perf measurement of a running native iOS app.
Works with
---
name: native-ios-perf-testing
description: Test native iOS app performance autonomously from the CLI — measure FPS during scroll/interaction, drive touch input, capture screenshots, stream runtime logs, and run before/after A/B comparisons of code changes. Use this whenever the user reports iOS UI jank, dropped frames, choppy scroll, animation hitches, "feels slow", layout jitter, or asks to validate that a perf change actually helped on iOS — *especially* in React Native / Expo apps. Also use when the user wants to drive the iOS Simulator from the command line (taps, swipes, scrolls), wants to verify a visual change without manual interaction, or asks how to test something "without me having to do it manually". Triggers include phrases like "60fps", "scroll feels janky", "test on simulator", "automate iOS testing", "FPS counter", "is my list smooth", "before/after on iOS", and references to xcrun, simctl, idb, or xctrace. Do NOT use for web apps (use a browser preview instead) or for unit/integration test runners — this skill is specifically for live perf measurement of a running native iOS app.
license: MIT
---
# Native iOS Perf Testing from the CLI
## When to use this skill
Reach for this when you need to **measure or validate iOS performance** without manual interaction. Typical situations:
- User reports janky scroll, dropped frames, or layout jitter in an iOS app.
- User just made a perf-related code change and wants proof it helped.
- User wants Claude to test on the simulator rather than asking them to scroll manually.
- User wants screenshots, logs, or numeric FPS data captured from a running iOS app.
The headline workflow is **measure FPS during a programmatically-driven scroll, before and after a code change**. Everything else (toolchain setup, screenshots, log streaming) supports that.
## Important constraint: simulator vs. real device
The simulator runs the JS thread on host CPU, so absolute fps numbers won't match a device. But the *shape* of perf problems — JS thread blocking, layout-shift jitter, recycler stalls — reproduces faithfully because it's the same JS code paths. Treat simulator FPS as a proxy for "did my change make things better or worse," not as a device-accurate fps reading.
If the user has a physical device and wants device-accurate hitch counts, see `references/xctrace-on-simulator.md` — there are real-device-only Instruments templates that work.
## Toolchain (one-time setup)
You need three things on the host machine: Xcode CLI tools (Apple-provided), `idb` + `idb_companion` (Facebook's CLI for driving the sim), and a way to inject a tiny FPS counter into the app under test.
If `idb_companion` isn't installed, set it up before doing anything else. Full instructions in `references/toolchain-setup.md` — read that file the first time you use this skill on a machine, or whenever an idb command fails with "bad interpreter" or "No Companion Connected". Subsequent runs on the same machine can skip that file.
## Core workflow: FPS A/B comparison
This is the workflow the skill is built around. It produces a clear before/after answer to "did the change help."
### Step 1 — Inject an FPS counter at the app entry point
Add a temporary `requestAnimationFrame` loop to the app's root layout (e.g., `app/_layout.tsx` for Expo Router, `App.tsx` for plain RN). Gate it on `__DEV__` so it never reaches production:
```tsx
// TEMP FPS instrumentation — remove after verification.
if (__DEV__ && typeof requestAnimationFrame !== "undefined") {
let fpsFrames = 0;
let fpsLast = Date.now();
const fpsTick = (): void => {
fpsFrames++;
const now = Date.now();
if (now - fpsLast >= 1000) {
console.log(`[FPS] ${fpsFrames} (interval=${now - fpsLast}ms)`);
fpsFrames = 0;
fpsLast = now;
}
requestAnimationFrame(fpsTick);
};
requestAnimationFrame(fpsTick);
}
```
The `interval=Xms` field is the part that matters most. RAF callbacks should fire ~16.67ms apart on a healthy 60fps thread, and the counter logs once per second. **If `interval` stretches to 1500ms+, that's the smoking gun: the JS thread was blocked for the entire delta**. A "60 FPS" reading with a 1000ms interval is healthy; a "4 FPS" reading with a 1555ms interval is catastrophic JS-thread blocking — the only reason you got 4 callbacks at all is that the thread eventually unblocked.
Why this works: `requestAnimationFrame` callbacks run on the JS thread in React Native. When list rendering or layout work blocks JS, RAF starves. So this counter is a direct, sensitive signal for JS-thread responsiveness during scroll — exactly the dimension users feel as "choppy."
### Step 2 — Make sure Metro is serving the change and the app is connected
The dev-built simulator app fetches its bundle from Metro at `http://localhost:8081` on launch. Verify Metro is up and that bundling your edits doesn't error:
```bash
# Is Metro running?
lsof -i :8081 -P -sTCP:LISTEN
# Does the iOS bundle compile cleanly with current source?
curl -s -o /tmp/ios-bundle.js -w "status=%{http_code} bytes=%{size_download}\n" \
"http://localhost:8081/node_modules/expo-router/entry.bundle?platform=ios&dev=true&minify=false"
```
A 200 status means your edits compiled. A 404/500 means there's an error in the source — fix it before running the test, otherwise the simulator will load stale code. (For non-Expo-Router apps, the entry path differs — look for a `main` field in `package.json` or the file matching `index.js`/`App.tsx`.)
If the user already has Metro running you'll see it on port 8081. If not, start it:
```bash
cd <app-dir> && bun run start --dev-client &
```
### Step 3 — Run the "after" measurement
```bash
# Kill any prior log capture so output is clean
pkill -f "log stream.*FPS" 2>/dev/null
# Reload the app to pick up your edits, give it ~6s to settle on the target screen
xcrun simctl terminate booted <bundle.id>
sleep 1
xcrun simctl launch booted <bundle.id>
sleep 8
# Stream FPS lines to a file in the background
rm -f /tmp/fps-after.log
xcrun simctl spawn booted log stream --level=info \
--predicate 'process == "<AppProcessName>" && eventMessage CONTAINS "[FPS]"' \
> /tmp/fps-after.log 2>&1 &
LOGGER_PID=$!
# Drive the interaction you want to measure (scroll three swipes, 1s apart)
for i in 1 2 3; do
idb ui swipe 200 650 200 200 --duration 0.3 \
--udid <BOOTED_UDID> > /dev/null 2>&1
sleep 1
done
sleep 2 # let post-scroll deceleration finish
kill $LOGGER_PID 2>/dev/null
grep -oE "\[FPS\][^()]*\([^)]*\)" /tmp/fps-after.log
```
Notes:
- The bundle id for `simctl` (e.g. `<bundle.id>`) and the **process name** for `log stream` (e.g. `TheApp`) are different. Find both with `xcrun simctl listapps booted | grep -B1 <known-string>`.
- `idb ui swipe` takes start_x start_y end_x end_y in **points** (not pixels). Get screen points from `idb describe`.
- Always `sleep 1` between swipes so the inertial scroll resolves before the next gesture, otherwise the second swipe rides the inertia of the first.
### Step 4 — Run the "before" measurement
Stash your changes, reload the app so the simulator pulls the old bundle from Metro, run the same scroll, capture to `/tmp/fps-before.log`. Then `git stash pop` to restore.
```bash
git stash push -- <changed-files>
# ... reload app and re-run Step 3, saving to fps-before.log ...
git stash pop
```
**Critically: relaunch the app between runs**, not just between code changes. Continuous scrolling sessions hit the bottom of the list and stop measuring real work. A fresh launch resets the scroll position and gives you a clean baseline.
### Step 5 — Read the comparison
Put both log outputs side by side. The fingerprint of a JS-thread blocking regression is:
- Idle: 60 FPS, interval ~1000ms (healthy)
- During scroll on the bad version: drops to 45/30/4 fps **with intervals stretching to 1500ms+**
- During scroll on the good version: stays 55-60 fps **with intervals staying near 1000ms**
The interval stretching is more diagnostic than the frame count — it directly shows JS thread block duration.
### Step 6 — Revert the instrumentation
Remove the FPS counter from `_layout.tsx` (or wherever you injected it) before declaring the task done. Run `git status` to confirm only the intended files are modified.
## Driving the simulator from CLI
The full set of useful commands lives in `references/idb-cookbook.md`. Read it when you need anything beyond a basic vertical scroll — taps, text input, hardware buttons, or descriptive accessibility queries.
The minimum set you'll use most:
```bash
# What's booted?
xcrun simctl list devices booted
# Apps installed on the booted sim
xcrun simctl listapps booted | grep -iE "(CFBundleIdentifier|YourApp)"
# Launch / terminate
xcrun simctl launch booted <bundle.id>
xcrun simctl terminate booted <bundle.id>
# Screenshot (PNG to disk; use Read to view)
xcrun simctl io booted screenshot /tmp/sim.png
# Stream app logs (filter by process and substring to keep volume sane)
xcrun simctl spawn booted log stream --level=info \
--predicate 'process == "<AppProcessName>" && eventMessage CONTAINS "<token>"'
# idb (after companion is connected — see references/toolchain-setup.md)
idb ui tap <x> <y> # tap at point (in pts)
idb ui swipe <x1> <y1> <x2> <y2> --duration 0.3
idb ui text "hello"
idb ui button HOME
idb describe # screen size in pts (use for swipe coords)
```
## What does NOT work on the simulator
Save yourself an hour: `xctrace record` with the **Animation Hitches** template returns "Hitches is not supported on this platform" on simulator. Also, `xctrace record --template "Time Profiler" --device <UDID> --attach <PID>` against a simulator process tends to **hang indefinitely** and produce empty traces — this is true across recent Xcode versions for several common templates. If you find yourself fighting xctrace + simulator, stop and use the RAF approach above. See `references/xctrace-on-simulator.md` for what does work, including paths that require a real device.
## Visual verification (when the change is observable, not measurable)
For "did the layout end up correct" rather than "is it fast", the workflow is simpler:
1. Reload the app: `xcrun simctl terminate booted <id> && xcrun simctl launch booted <id>`
2. Wait for the relevant screen to settle (`sleep 6-8`)
3. `xcrun simctl io booted screenshot /tmp/sim.png`
4. Open the file with the `Read` tool to see what's on screen.
If you need to navigate first, use `idb ui tap` or `idb ui swipe`. Combine with screenshots between steps to confirm you're on the right screen.
## A few things to internalize
- **Reload between every test run.** The most common reason for a misleading result is leftover state (scroll position, modal open, ad already loaded).
- **The `interval` field is the canary.** A 60 fps reading with stretched intervals means the thread was *idle* most of the second after a long block — measure the worst stretch, not the average.
- **Don't trust your screenshot for jitter.** Visual jitter is a temporal phenomenon — a still image can't show "items jumped 4 pixels and snapped back." For those symptoms, go to the code (FlashList's `maintainVisibleContentPosition`, async-loading components that grow, nested measurement) rather than looking for them in stills.
- **Be honest about limits.** If a perf claim isn't measurable from CLI on the sim, say so directly. The user can scroll manually on a device for the final gut check; you've narrowed the search space enough that they don't have to do it blind.More Mobile skills
animation-vocabulary
emilkowalski/skills
Reverse-lookup glossary that turns a vague description of a web animation or motion effect into its exact term ("the bouncy thing when a popover opens" → Pop in; "the iOS rubber-band scroll" → Rubber-banding). Use when the user asks "what's it called when…", or describes a motion effect without knowing its name and wants the right word to prompt an AI or designer with. For naming an effect, not designing or building one.
xcode-project-setup
firebase/agent-skills
Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).
cross-border-ecommerce
nexscope-ai/ecommerce-skills
Cross-border e-commerce expansion advisor. Scores target markets on 8 weighted dimensions (market size, ecommerce penetration, competition, regulatory complexity, logistics infrastructure, payment ecosystem, cultural distance, IP protection), compares 5 fulfillment models with cost and transit data, provides country-by-country tax/duty compliance guides (EU VAT/IOSS, UK VAT, US sales tax, CA GST, AU GST, JP consumption tax), maps local payment preferences by market, and builds a phased expansion roadmap. No API key required.

