Skip to content
Blog

Firebase Performance Monitoring in Flutter: Traces, Screens, and Network Metrics

Install firebase_performance, get app start and HTTP traces for free, then add the Flutter-specific pieces: per-screen traces, slow-frame counters, and console filters that answer real questions.

Published on • October 17, 2026

AI Assistant

DevTools tells you how your app behaves on your machine, in profile mode, with a debug cable attached. It cannot tell you that checkout takes four seconds on a three-year-old Android in a weak network. That gap is the whole reason field monitoring exists, and Firebase Performance Monitoring is the option most Flutter teams already have wired into their Firebase project.

The trap is that “adding the SDK” is roughly half the job on Flutter. Once firebase_performance is in, Firebase starts collecting app start time and every HTTP request your app makes - and then stops short, because Flutter renders into a single native view controller. The native SDK cannot see your Dart route changes, so per-screen rendering traces, the feature that makes the native product useful, never appear.

This post walks the full path: setup, what you get for free, how to rebuild the missing screen layer yourself with custom code traces, how to count frame jank Firebase cannot natively observe, and how to read the console without drowning in aggregated URL patterns.

Setup

Prerequisites are the standard ones: a Firebase project with Firebase configured and initialized in your Flutter app.

$ flutter pub add firebase_performance
$ flutterfire configure
$ flutter run

flutterfire configure is doing real work here beyond generating firebase_options.dart: on Android it adds the required Performance Monitoring Gradle plugin to your app. Skip it and your events never leave the device. After a rebuild, the SDK automatically starts collecting data for your app’s lifecycle (app start time) and for HTTP/S network requests.

Two side effects worth knowing before you commit:

  • Adding Performance Monitoring pulls in the Remote Config SDK as a dependency. If you were planning to adopt Remote Config anyway, this is free; if you assumed the dependency graph was minimal, it is not.
  • The plugin publishes for Android, iOS, and web (version 0.12.0 as of this writing). Desktop targets are not in the platform list, so guard any shared code accordingly.

To generate events during development, background and foreground the app a few times, navigate around, and fire some requests. Events are batched locally and uploaded periodically - roughly every 30 seconds, or when the app returns to the foreground - so the console lags your taps by design. Initial data usually shows up within a few minutes; if nothing arrives, the troubleshooting page in the console covers the usual “SDK detected no data” cases.

What you get before writing a line

DataNative iOS/AndroidFlutter
App start time and foreground/background tracesYes, automaticYes, automatic
HTTP/S network request tracesYes, automaticYes, automatic
Screen rendering traces per screenYes, automaticNo - one native view controller hosts the whole Flutter app, so the native SDK never sees Dart screen transitions
Page load tracesWeb onlyN/A
Custom code traces and metricsYesYes - this is where a Flutter app rebuilds what it lost
Custom network tracesYesYes (native only; the Dart newHttpMetric API is a no-op stub on web)

The screen row is the one that surprises teams. You migrate from native, the dashboard looks thinner, and the instinct is that the SDK is misconfigured. It is configured; the granularity simply does not exist at the native layer.

Rebuilding the screen layer with custom traces

The fix is a custom code trace per screen. Firebase is explicit about the rules of the road: a trace’s default metric is duration, you can run several traces concurrently, names must have no leading or trailing whitespace, no leading underscore, and be at most 100 characters, and you should avoid starting and stopping traces at high frequency (once per frame in a game is called out as the anti-pattern).

A NavigatorObserver is the cleanest hook, because Dart route changes are exactly the events the native SDK misses:

import 'package:firebase_performance/firebase_performance.dart';
import 'package:flutter/material.dart';

class ScreenTraceObserver extends NavigatorObserver {
  Trace? _current;

  Future<void> _open(Route<dynamic>? route) async {
    await _current?.stop();
    _current = null;
    if (route == null) return;

    final label = route.settings.name ?? route.runtimeType.toString();
    final name = 'screen_${label.replaceAll(RegExp(r'[^A-Za-z0-9]'), '')}'
        .substring(0, 100);

    final trace = FirebasePerformance.instance.newTrace(name);
    await trace.start();
    _current = trace;
  }

  @override
  void didPush(Route route, Route? previousRoute) => _open(route);

  @override
  void didPop(Route route, Route? previousRoute) => _open(previousRoute);

  @override
  void didReplace({Route? newRoute, Route? oldRoute}) => _open(newRoute);
}

Register it where your router lives:

MaterialApp(
  navigatorObservers: [ScreenTraceObserver()],
  // or GetMaterialApp / Router with the same observer list
);

Each screen now shows up in the Custom traces subtab with its own duration distribution, filterable by app version, device, and country like any other trace. Give routes real settings.name values ('/checkout' rather than an anonymous widget route) or the trace names collapse into runtime types.

Counting jank Firebase cannot see

Frame build and raster times are not part of Performance Monitoring’s automatic collection. Flutter’s own metrics documentation is the authority here: it recommends monitoring four statistics for frame buildDuration and rasterDuration - average, 90th percentile, 99th percentile, and worst - and exposes the raw numbers through FrameTiming (buildDuration, rasterDuration, totalSpan).

You do not have to choose between the two toolchains. Subscribe to the frame timings yourself and fold the result into the same custom trace as counters, which is precisely what custom metrics are for (Firebase’s own examples are cache hits and retries):

import 'package:flutter/scheduler.dart';

class SlowFrameReporter {
  SlowFrameReporter(this._trace) {
    SchedulerBinding.instance.addTimingsCallback(_onTimings);
  }

  final Trace _trace;

  void _onTimings(List<FrameTiming> timings) {
    for (final timing in timings) {
      if (timing.totalSpan.inMilliseconds > 16) {
        _trace.incrementMetric('frames_over_16ms', 1);
      }
      if (timing.buildDuration.inMilliseconds > 16) {
        _trace.incrementMetric('build_over_16ms', 1);
      }
    }
  }

  void dispose() {
    SchedulerBinding.instance.addTimingsCallback((_) {});
  }
}

Now the checkout screen trace carries both how long the screen took and how many frames during it blew the 16 ms budget, side by side in the console, filterable by device class. That combination - duration plus jank count per screen, in the field - is something neither DevTools nor Firebase gives you alone.

Watch the budget while you do this: each custom trace records up to 32 metrics including the default duration, and each trace accepts up to 5 custom attributes.

Network traces and URL patterns

Every request your app issues gets an HTTP/S network request trace automatically, collecting four metrics:

  • Response time - request issued until the response is fully received
  • Response payload size - bytes downloaded
  • Request payload size - bytes uploaded
  • Success rate - responses in the 100-399 range over total responses

Requests are aggregated under URL patterns such as example.com/*/animals/**. Firebase derives automatic patterns from your traffic, tries your custom patterns first when you have defined any, and maps each request to exactly one pattern. Two practical consequences:

  • Aggregation is forward-looking. When a new automatic pattern is derived, past data is not re-aggregated, so a dashboard that looks inconsistent across a period often reflects pattern evolution rather than a traffic change.
  • Automatic patterns are dynamic. If you care about a specific endpoint - the payment call, the search API - define a custom URL pattern and it wins over the derived ones. You can also configure how success rate is calculated for endpoints where an error code is expected and handled.

Filtering is where the dashboard starts answering questions rather than displaying curves: by app version to compare releases, by device to see whether older hardware is the problem, by country to rule out a region’s network. The metrics board at the top of the Performance dashboard pins your key numbers with week-over-week deltas, and alerts can email the team when, say, response time for a URL pattern crosses a threshold.

Cost, quotas, and the sanity limits

The per-trace limits are the ones that bite in code review, so keep them in the PR checklist:

LimitValue
Metrics per custom trace (including duration)32
Custom attributes per trace5
Trace and metric name rulesNo leading/trailing whitespace, no leading underscore, max 100 characters
Attribute name rulesA-Z, a-z, _ only, no spaces, max 32 characters
High-frequency start/stopDiscouraged - do not create traces per frame
PII in custom attributesProhibited; data exposing personally identifiable information is subject to deletion without notice

Beyond the hard numbers, the real cost control is volume discipline: a trace per screen per navigation is cheap, a trace per list item or per frame is not. For current pricing and plan-level quotas, check the Firebase pricing page rather than a blog post - the numbers move.

Gotchas worth knowing before you ship

  • No data on a fresh install. Check that flutterfire configure added the Android Gradle plugin, then look for the log tags: FirebasePerformance on Android, Firebase/Performance on iOS, with lines like Logging trace metric: TRACE_NAME, FIREBASE_PERFORMANCE_CONSOLE_URL. Clicking that URL opens the exact trace in the console.
  • Debug vs release. Validate on a real device or emulator and interact with the app before judging the dashboard; batching means an idle app sends nothing.
  • Collection can be toggled at runtime. FirebasePerformance.instance.setPerformanceCollectionEnabled(false) disables it, which is useful for a privacy setting or a consent screen - wire it to your app’s actual consent state rather than leaving it default.
  • Anonymous routes produce anonymous traces. If screen traces show up as widget runtime types, name your routes.
  • Always pair start with stop. The Flutter API docs recommend calling start() and stop() on every created trace to avoid leaking on the platform side; a try/finally around awaited work is the cheapest insurance.
  • Screen traces stop at the Dart layer. Native view controllers embedded via platform views do not start Flutter traces. For add-to-app or heavily hybrid screens, instrument from the native side too.

Checklist

  • firebase_performance added and flutterfire configure run, confirming the Android Gradle plugin was inserted.
  • App exercised on a device or emulator; batching understood (about 30 seconds or foregrounding) before declaring “no data”.
  • A NavigatorObserver starting and stopping one custom trace per named route.
  • Slow-frame counters incremented into the screen trace from an addTimingsCallback, mirroring Flutter’s recommended average/90th/99th/worst frame statistics.
  • Custom URL patterns defined for business-critical endpoints; success-rate calculation reviewed for endpoints where errors are expected.
  • Trace and attribute names within the documented limits; no PII in attributes.
  • Metrics board populated with the four or five numbers the team actually watches, plus an alert on a regression threshold.
  • Collection toggling wired to the app’s consent state.

Sources: Get started with Performance Monitoring for Flutter, Add custom monitoring for specific app code, Learn about HTTP/S network request performance data, Firebase Performance Monitoring overview, firebase_performance on pub.dev, Performance metrics