Ux metrics
Skill almasumdev/awesome-mobile-observability-agent-skills/.github/skills/performance/ux-metrics
Measure mobile UX quality with frozen frames, slow frames, ANRs, app hangs, and interaction-to-next-paint style metrics. Use when diagnosing jank, adopting JankStats / MetricKit, or reporting UX health.From its SKILL.md
npx -y skills add almasumdev/awesome-mobile-observability-agent-skills --skill ux-metricsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.6 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it
UX Performance Metrics on Mobile
Instructions
Startup time is only part of the story. UX quality is dominated by jank, frozen frames, ANRs (Android) and app hangs (iOS). Measure them directly on the device and roll them up into release-over-release dashboards.
1. Definitions
- Slow frame: a frame that took longer than the target frame interval. Baseline 16 ms at 60 Hz, 8.3 ms at 120 Hz.
- Frozen frame: a frame that took > 700 ms (Android), > 0.7 s (iOS). Always user-noticeable.
- Jank: subjective; operationally "frame missed deadline by > 2x".
- ANR (Android): app did not respond to input for >= 5 s (or a broadcast within 10 s). Google Play measures ANR rate per daily user.
- App hang (iOS): main thread blocked >= 250 ms. Reported via MetricKit as
MXAppResponsivenessMetric. - INP-style metric (mobile): from input event to visible feedback. No platform-standard name yet; model as
ui.interaction.duration_mshistogram.
2. Android -- JankStats
JankStats is the first-party AndroidX API for frame timing. It uses FrameMetrics on API 24+ and gracefully degrades below.
class UxMetricsReporter(activity: Activity, private val otel: OpenTelemetry) {
private val histogram = otel.getMeter("ux").histogramBuilder("mobile.frame.duration_ms")
.setUnit("ms").build()
private val frozenCounter = otel.getMeter("ux").counterBuilder("mobile.frame.frozen.count").build()
private val jank = JankStats.createAndTrack(activity.window) { frame ->
val ms = frame.frameDurationUiNanos / 1_000_000.0
val attrs = Attributes.of(
AttributeKey.stringKey("screen"), activity::class.java.simpleName,
AttributeKey.booleanKey("is_jank"), frame.isJank
)
histogram.record(ms, attrs)
if (ms > 700) frozenCounter.add(1, attrs)
}
fun onResume() { jank.isTrackingEnabled = true }
fun onPause() { jank.isTrackingEnabled = false }
}
3. iOS -- MetricKit + CADisplayLink
Use MetricKit for daily rollups and a CADisplayLink sampler for granular jank signals.
final class FrameSampler {
private var link: CADisplayLink?
private var lastTs: CFTimeInterval = 0
private let histogram = meter.createDoubleHistogram(name: "mobile.frame.duration_ms", unit: "ms")
func start() {
let link = CADisplayLink(target: self, selector: #selector(tick))
link.add(to: .main, forMode: .common)
self.link = link
}
@objc private func tick(_ link: CADisplayLink) {
if lastTs != 0 {
let delta = (link.timestamp - lastTs) * 1000
histogram.record(value: delta, labels: ["screen": .string(currentScreenName())])
}
lastTs = link.timestamp
}
func stop() { link?.invalidate(); link = nil; lastTs = 0 }
}
For hangs, rely on MetricKit.MXAppResponsivenessMetric + the Sentry AppHangTracking or Firebase's hang signal. Custom watchdog threads that kill the process are not recommended for production.
4. Flutter -- SchedulerBinding and DevTools
class FrameMetrics {
static void enable() {
SchedulerBinding.instance.addTimingsCallback((timings) {
for (final t in timings) {
final buildMs = t.buildDuration.inMicroseconds / 1000.0;
final rasterMs = t.rasterDuration.inMicroseconds / 1000.0;
Sentry.metrics().distribution('mobile.frame.build.ms', value: buildMs);
Sentry.metrics().distribution('mobile.frame.raster.ms', value: rasterMs);
if (buildMs + rasterMs > 700) {
Sentry.metrics().increment('mobile.frame.frozen.count');
}
}
});
}
}
Prefer Impeller (default on iOS, rolling to Android) for consistent frame times.
5. React Native -- Perf Monitor + Sentry
- Enable Sentry's
ReactNavigationInstrumentationso screen transitions emit frame metrics. - Use the
react-native-performancepackage orPerformanceObserverforlongtask-style signals. - Track the "slow scroll" signal by instrumenting
FlatListonScrolldeltas againstrequestAnimationFrameintervals.
6. Interaction-to-Next-Paint (INP-like)
Instrument the user intent explicitly: from the onPressIn / touchesBegan / onPointerDown to the first re-render where the user sees feedback.
val span = tracer.spanBuilder("ui.interaction.checkout_button").startSpan()
button.setOnClickListener {
span.addEvent("pressed")
onCheckoutClicked { span.addEvent("first_feedback_rendered"); span.end() }
}
Aim for p75 <= 100 ms, p95 <= 250 ms on mid-tier devices.
7. ANR Signals and Mitigation
- Capture ANR counts via Crashlytics/Sentry. Correlate with release and device class.
- Correlate ANRs with the
last_screencustom key andlast_operationmetric to pin down what blocked the main thread. - Usual culprits: large image decoding on main thread, synchronous disk I/O, uncached JSON parsing of multi-MB payloads, heavy string formatting in
Log. - Mitigations:
CoroutineDispatcher.IO/Dispatcher.main.async/ Isolates /InteractionManager.runAfterInteractions.
8. Dashboards
- "UX Health" dashboard per platform: p50/p75/p95 frame time by screen, frozen frame rate, ANR rate, hang rate, interaction duration by critical interaction.
- A Top jank screens table ranks screens by (slow frames * sessions on screen).
- A Regression lens overlays the current release vs last two releases.
9. Targets
- Slow frame rate: < 5% of frames.
- Frozen frame rate: < 0.1%.
- ANR rate: < 0.47% of DAU (Google Play cutoff), target < 0.1%.
- App hang rate: < 0.5%.
- p95 critical interaction: < 250 ms on mid-tier.
10. Release Gating
- Block rollout when the UX dashboard shows a regression > 15% in any of the above metrics vs the previous release at the same rollout stage.
- Feature-flag expensive UI changes so you can turn them off remotely if UX regresses.
Checklist
- JankStats (Android) / MetricKit + CADisplayLink (iOS) /
addTimingsCallback(Flutter) / Perf Monitor (RN) are wired in. - ANRs and hangs are captured through Crashlytics or Sentry and correlated with last screen / operation.
- Interaction-to-next-paint spans cover critical buttons.
- Frozen frame count, ANR rate, hang rate are sliced by release and device class.
- Top-jank-screens and regression dashboards exist.
- Explicit targets are documented and tied to rollout gates.
- Release gating halts rollout on > 15% regressions.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.