frame_insights 0.1.0 copy "frame_insights: ^0.1.0" to clipboard
frame_insights: ^0.1.0 copied to clipboard

In-app frame timing for Flutter: a live FPS HUD, a frame-time chart, and a dashboard that ties each janky frame to the traced work that produced it.

frame_insights #

In-app frame timing for Flutter: a live FPS window, a frame-time chart, and a dashboard that ties each janky frame to the traced work that produced it.

pub package license: BSD-3-Clause

Why another performance overlay #

There are several packages that put FPS and memory on screen. Most of them stop at "this frame was slow". frame_insights is built around the next question: which code made it slow?

  • Build vs raster split. Every frame is drawn as stacked bars so you can see whether the UI thread or the raster thread is the problem.
  • Jank attribution. Wrap the work you care about in PerformanceTracer and each janky frame records what was running while it was produced.
  • Scenes from routes. A NavigatorObserver labels every measurement with the route it belongs to, so a report says feed_detail, not "some frame".
  • Startup measurement. Time to first frame is captured from before runApp, so app launch is part of the same picture.
  • Health ranges. Jank rate and first-frame time are classified against documented thresholds, so a number is either fine or visibly not.

Install #

dependencies:
  frame_insights: ^0.1.0

Quick start #

Three steps, in main() and around your MaterialApp:

import 'package:frame_insights/ui.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 1. Report frames into the store, and start collecting before runApp so the
  //    startup measurement includes everything up to the first frame.
  PerformanceMonitorStore.instance.attach(PerformanceMonitor.instance);
  await PerformanceHudController.instance.restore();
  PerformanceMonitor.instance.start(scene: 'app_startup');
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: _navigatorKey,
      // 2. Label measurements with the route that produced them.
      navigatorObservers: [PerformanceRouteObserver()],
      // 3. Float the HUD above the Navigator so it survives navigation.
      builder: (context, child) => PerformanceHud(
        store: PerformanceMonitorStore.instance,
        controller: PerformanceHudController.instance,
        onOpenDashboard: () =>
            _navigatorKey.currentState?.push(performanceDashboardRoute()),
        child: child ?? const SizedBox.shrink(),
      ),
      home: const HomePage(),
    );
  }
}

final _navigatorKey = GlobalKey<NavigatorState>();

Debug and profile builds start with the window visible; release builds keep it hidden until your app turns it on (PerformanceHudController.instance ..setVisible(true)), which is what a "developer settings" switch usually wires up.

The three surfaces #

Surface Use it to
PerformanceHud Watch live numbers while using the app: FPS, jank rate, build/raster, startup. Draggable and collapsible.
FrameTimeChart See the last N frames as stacked bars, with everything over budget highlighted.
PerformanceDashboardPage Read trends: overview, frame-time chart, sampling info, jank attribution, traced-work ranking, and per-interval reports.

Instrumenting your code #

Frame timings say that a frame was slow. Tracing says what was running:

// One synchronous region.
final layout = PerformanceTracer.instance.trace('feed.layout', computeLayout);

// One asynchronous region, including the awaited work.
await PerformanceTracer.instance.traceAsync('video.init', controller.initialize);

// A region that spans callbacks.
final scope = PerformanceTracer.instance.span('image.decode');
// ... later
scope.stop();

// An event that has no useful duration, such as a rebuild.
PerformanceTracer.instance.tick('build:FeedPostCard');

PerformanceRebuildCounter does the last one for you:

itemBuilder: (context, index) => PerformanceRebuildCounter(
  name: 'FeedPostCard',
  child: FeedPostCard(post: posts[index]),
),

Keep traced regions coarse — one per meaningful operation (page load, network call, video initialization) rather than one per widget. Tracing is disabled in release builds so the measurements are not skewed by their own bookkeeping.

Scenes and routes #

PerformanceRouteObserver reports the visible route so reports and jank records say which page they belong to. Give your routes a name:

Navigator.of(context).push(
  MaterialPageRoute<void>(
    settings: const RouteSettings(name: 'checkout'),
    builder: (_) => const CheckoutPage(),
  ),
);

Unnamed routes are reported as home (the root) or unnamed_route. The dashboard route built by performanceDashboardRoute() is ignored by default, so opening the dashboard does not restart the measurement you are looking at. Entering a new scene ends the previous report — one report per page visit.

Diagnosing a janky frame #

Work through the three levels the UI gives you, in order:

  1. Thread — the jank card says build/layout bound or raster/paint bound. Build-bound points at rebuilds, layout, synchronous work, image decoding. Raster-bound points at complex painting, blur and shadows, oversized images, first-run shader compilation.
  2. Scene — the card, the report, and the HUD header all carry the route name, so you can tell which page is responsible.
  3. Traced work — the card lists the tracer samples recorded while that frame was produced, slowest first. If it says the frame had no traced work, that code is not instrumented yet.

For anything deeper, use DevTools (flutter run --profile, then the Performance page): it can see individual build/layout/paint calls, image decoding, and shader compilation, which an in-app monitor cannot.

Health ranges #

PerformanceHealth classifies two metrics, and the UI paints anything outside the normal range amber or red:

Metric Normal Elevated Critical
Jank rate ≤ 5% 5% – 15% > 15%
First frame ≤ 1.5 s 1.5 – 3 s > 3 s

The thresholds are deliberately loose and are meant to be tuned: what counts as healthy depends on the build flavour and the device class. Edit the constants in PerformanceHealth to match a baseline for the devices you target.

Two things to know when reading them:

  • Jank rate is sensitive to the frame budget. The budget comes from the display refresh rate, so on a 120 Hz panel it is 8.3 ms and the same code scores a much higher rate than on 60 Hz.
  • First frame is measured from the start of collection, so it includes whatever your app does before runApp — opening a database or a network session, for example. That is intentional: it is the delay the user waits through.

Theming and languages #

Every colour and every user-visible string is resolved from the ambient theme. Register a PerformanceTheme in your ThemeData to restyle or translate:

MaterialApp(
  theme: ThemeData(
    extensions: const [
      PerformanceTheme(
        // Ships with `english` (default) and `chinese`.
        strings: PerformanceStrings.chinese,
        buildColor: Color(0xFF4C8DFF),
        badColor: Color(0xFFD8674B),
      ),
    ],
  ),
)

Without a registered theme the widgets fall back to the package defaults: fixed series colours for the chart, the app's ColorScheme for surfaces and accents, and English text. PerformanceStrings is a plain immutable class — build your own preset with PerformanceStrings.english.copyWith(...) or from scratch.

API map #

Import package:frame_insights/frame_insights.dart for the layer that has no Material dependency, or package:frame_insights/ui.dart for everything including the widgets.

CorePerformanceMonitor (timings callback), PerformanceMonitorStore (rolling window, live metrics), PerformanceMonitorConfig (budget, report interval), FrameMetrics, PerformanceReport, PerformanceJankRecord, PerformanceTracer, PerformanceRebuildCounter, PerformanceRouteObserver, JankCalculator, PerformanceHealth, PerformanceStrings.

UIPerformanceHud, PerformanceHudController, FrameTimeChart, PerformanceDashboardPage, performanceDashboardRoute(), performanceDashboardRouteName, PerformanceTheme.

Limitations #

  • Timings only exist on a device or simulator. The web build gets no frame timings and the chart shows a waiting state. Debug builds carry the framework's own overhead, so absolute numbers are inflated; profile builds are the closest to what users feel.
  • "FPS" is derived, not presented. It is the rate implied by the average frame span, not a count of frames the platform actually displayed. On a page that is not animating very few frames are produced, so a handful of expensive rebuilds can dominate the window and the reading drops even though nothing looks stuck.
  • Attribution is temporal. Tracer samples are recorded just before the engine's callback, so they are attached to the frames of that batch, and only the first janky frame of a batch receives them — the same batch is not counted twice.
  • Only the UI thread is traced. Raster-thread cost is only visible as the build-versus-raster split in the chart.
  • Rolling windows. 240 frames, 12 reports, 20 jank records, and 40 traced names are kept by default; the oldest entries are dropped. Set the capacities on PerformanceMonitorStore and PerformanceTracer to change that.
  • The monitor measures itself. A page that rebuilds on every store notification — starting with the dashboard, which is why opening it makes the jank rate look bad — is part of what gets measured. Evaluate performance on the pages you are investigating, not with the dashboard open.

Example #

example/ contains a minimal app: a counter, the floating window, and a button that opens the dashboard.

License #

BSD-3-Clause. See LICENSE.

0
likes
160
points
28
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

In-app frame timing for Flutter: a live FPS HUD, a frame-time chart, and a dashboard that ties each janky frame to the traced work that produced it.

Repository (GitHub)
View/report issues

Topics

#flutter #performance #profiling #debugging #fps

License

BSD-3-Clause (license)

Dependencies

flutter, shared_preferences

More

Packages that depend on frame_insights