Skip to content
Blog

Measuring Flutter Web Performance: From DevTools to Real Users

Monitor Flutter web performance end to end: timeline flags and profile mode in Chrome DevTools, frame and bundle metrics, Core Web Vitals via JS interop, budgets, and a monitoring checklist.

Published on • November 11, 2026

AI Assistant

Profiling tells you why a Flutter web app is slow; monitoring tells you whether it is still fast for the people using it. The two halves live in different places — Chrome DevTools on your machine, and real-user telemetry in production — and both need the same numbers to compare against. This is the Flutter-specific pipeline; the metric thresholds and RUM plumbing are covered in the Core Web Vitals and observability posts.

Start in the lab: profile mode in Chrome DevTools

Flutter’s framework emits timeline events while it builds frames and draws scenes, and those events surface in Chrome DevTools’ Performance panel (profiling needs Flutter 3.14 or later). The docs specify profile mode for this, because debug mode’s extra checks distort the timings — run a profile build, then optionally switch on the tracing flags from main():

import 'package:flutter/rendering.dart'
    show debugProfileLayoutsEnabled, debugProfilePaintsEnabled;
import 'package:flutter/widgets.dart'
    show debugProfileBuildsEnabled, debugProfileBuildsEnabledUserWidgets, runApp;

void main() {
  if (bool.fromEnvironment('TIMELINE')) {
    debugProfileBuildsEnabled = true;          // every widget build
    debugProfileBuildsEnabledUserWidgets = true; // your widgets specifically
    debugProfileLayoutsEnabled = true;         // every RenderObject layout
    debugProfilePaintsEnabled = true;          // every RenderObject paint
  }
  runApp(const MyApp());
}

Wire the switch to --dart-define so the flags never live in source you ship:

flutter build web --profile --dart-define=TIMELINE=true
python -m http.server -d build/web 8080

Serve the build, open Chrome DevTools’ Performance panel, hit record, and interact with the app. For the sections the framework cannot see — data fetching, image decoding, a slow shader compile — add your own events with dart:developer’s Timeline and TimelineTask; they land in the same recording.

While you are in the panel, three things are worth reading every time: which event pushed a frame over budget, how long the build, layout, and paint phases ran, and where the browser’s long tasks sit relative to your custom marks. If you want a number without opening DevTools at all, the PerformanceOverlay widget renders live frame-time bars inside the app itself and works on web like any other widget.

What to watch each release

Three groups of numbers, three different tools. Frames: build and paint/raster time per frame, and how often you blow the 16.7 ms budget — jank shows up as long bars in the recording. Bundles: the size of main.dart.js after minification and tree-shaking, plus any deferred chunks, watched from the Network panel so a regression shows up as a bigger download rather than a vague feeling. Loading: first contentful paint and largest contentful paint from Lighthouse. The companion performance-optimization post walks through hunting each of these down and fixing them.

Move to the field: real-user monitoring

Lab numbers come from one machine on one network. Field numbers come from your users’ devices, and they are what Core Web Vitals scoring uses: LCP within 2.5 seconds, INP at 200 ms or less, and CLS at 0.1 or less, each judged at the 75th percentile of page loads rather than the average. Collect them in the browser with the web-vitals library and ship them with navigator.sendBeacon, which survives the page unloading:

import { onLCP, onINP, onCLS } from 'web-vitals';

const report = (metric) => {
  navigator.sendBeacon('/rum', JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    release: window.__APP_VERSION__,
  }));
};

onLCP(report);
onINP(report);
onCLS(report);

Load that script from web/index.html, or drive it through package:web from Dart, and tag every event with your release version so you can compare a deploy against the previous one. The library is only a wrapper: underneath it, PerformanceObserver reads the browser’s performance entries, and you can subscribe to the same stream yourself for anything the library does not cover — long tasks, resource timings, or slow interactions:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) send('slow-interaction', entry.duration);
  }
}).observe({ type: 'event', durationThreshold: 40, buffered: true });

The buffered: true flag replays entries recorded before the observer was created, which is why it is safe to register it after the page has already loaded. If you would rather not build the collection endpoint yourself, a RUM SDK such as Sentry does the same thing and adds crash reporting — the important part is that it is sampled across real users and split by release, not just “the office connection”.

Budgets and a monitoring checklist

Pick numbers before you need them. This is a reasonable starting budget for a content-heavy Flutter web app:

MetricBudgetMeasured with
Initial JS (main.dart.js, gzipped)≤ 500 KBNetwork panel / CI
Frame build time≤ 8 msChrome DevTools Performance
Frame raster time≤ 8 msChrome DevTools Performance
LCP≤ 2.5 sRUM, 75th percentile
INP≤ 200 msRUM, 75th percentile
CLS≤ 0.1RUM, 75th percentile

Then work the checklist: cut one profile-mode build with the timeline flags on for every release and record a fixed interaction path; wrap slow sections in custom Timeline events; track main.dart.js size in CI and fail on regressions; ship web-vitals with the app and tag events with the release; alert on 75th-percentile values rather than averages; and keep a Lighthouse run on a clean profile as your lab baseline.

Lab and field data answer different questions. DevTools and Lighthouse are controlled and reproducible, which makes them the right tool for catching regressions and finding causes; RUM is messy and representative, which makes it the right tool for deciding what to fix first. Expect them to disagree — when they do, debug in the lab, but prioritize with the field.

Takeaway: Measure Flutter web twice: profile-mode Chrome DevTools recordings with the timeline flags for cause, and release-tagged web-vitals telemetry for truth — then hold both to a written budget.

Sources