Flutter performance: how to diagnose jank and FPS drops (checklist)

Jank is when scrolling or animations stutter—frames miss the rendering budget and the UI feels “rough.” In Flutter this usually shows up as random hiccups while scrolling a feed, opening a screen, or running animations that should be smooth.

The good news: in most apps you can diagnose jank systematically. This article is a practical checklist you can repeat: set up proper measurement (profile/release), identify whether the bottleneck is on the Dart/UI side or the raster/GPU side, read traces, then apply targeted fixes (build/layout/paint, lists, images, shaders, GC, isolates). Finally, you’ll get a short anti-regression routine so performance doesn’t degrade every sprint.

0) First decide: UI thread vs raster (GPU)

Flutter produces a frame in two main stages:

  • UI thread (Dart): build → layout → paint, producing a scene.
  • Raster thread (Skia/GPU): rasterizing and drawing to the screen.

If either side exceeds the per-frame budget, you get jank. The budget depends on refresh rate:

  • 60 Hz → ~16.6 ms per frame
  • 90 Hz → ~11.1 ms
  • 120 Hz → ~8.3 ms

Takeaway: on 120 Hz devices even “small” work becomes expensive. Your first step is always to determine whether Dart/UI work or raster/GPU work is the limiting factor.

1) Rule #1: measure in profile / release, not debug

Debug builds are slower (asserts, instrumentation, no full compiler optimizations). Diagnosing in debug often leads to false conclusions.

  • On device: flutter run --profile
  • Android: test on real hardware, not just the emulator.
  • iOS: test on a real device—GPU/CPU characteristics differ from the simulator.

Tip: if the issue is intermittent, create a reproducible script: e.g., “scroll the feed for 30 seconds, open details, go back, repeat.” Use the same script every time you measure.

2) 10-minute triage checklist

  1. Run in profile.
  2. Enable the Performance Overlay (DevTools or showPerformanceOverlay).
  3. Check where the spikes occur: top (UI) vs bottom (raster).
  4. In DevTools → Flutter frames: find the worst frames.
  5. Open Timeline and see what consumed time (build/layout/paint, shader compilation, image decoding, GC, etc.).

After this, you typically know which category of fix you need: widget rebuilds, paint-heavy effects, images, or CPU-heavy work.

3) Common culprit #1: too many rebuilds (build cost)

A small state change triggers rebuilding a large subtree. Symptoms: UI thread spikes, timeline shows heavy build work.

How to recognize it

  • DevTools: rebuild statistics / tracking widget rebuilds.
  • Your state management emits changes too frequently (e.g., a timer firing at 60 FPS).
  • Large “container” widgets rebuild children that didn’t actually change.

Fixes that actually move the needle

  • Split widgets: extract subtrees so only the necessary part rebuilds.
  • Use const where possible—this reduces build overhead.
  • With Provider/Riverpod/Bloc: use selectors (select, small Consumer widgets) instead of a single broad listener.
  • Never do heavy computations in build (sorting, mapping thousands of items). Precompute and cache.

4) Common culprit #2: expensive layout and paint

Even with a reasonable build cost, layout and paint can be expensive: deep widget trees, many shadows, clips, blurs, and translucency.

Warning signs

  • Timeline shows significant time in “Layout” or “Paint.”
  • Raster thread time grows with it.
  • Heavy use of Opacity, ClipRRect, BackdropFilter, ShaderMask.

Practical fixes

  • Avoid large BackdropFilter areas; if needed, restrict the region and keep it stable.
  • Be cautious with clipping. If you only need rounded corners for an image, consider preparing the asset or minimizing the clipped area.
  • Use RepaintBoundary to isolate frequently animated pieces from repainting the entire screen.
  • If you only need transforms/opacity, animate small subtrees and avoid relayouting large parts of the UI.

5) Lists and scrolling: where most jank lives

If stutters mostly happen while scrolling, start with lists. The mistakes are very repeatable.

List checklist

  • Use builders: ListView.builder, SliverList, GridView.builder.
  • Provide itemExtent or prototypeItem when item height is stable—this reduces layout work.
  • Avoid nesting ListView inside SingleChildScrollView unless you fully understand the trade-offs.
  • Avoid IntrinsicHeight/IntrinsicWidth inside list items—these can be extremely costly.
  • Images: decode/resize/cache properly (next section).

Typical improvement: slim down list items

Your list item should be “flat” and predictable: fewer layers, fewer effects. If you have complex cards (image + gradient + multiple text lines + buttons), consider:

  • pre-rendering static parts
  • isolating animations with RepaintBoundary
  • replacing heavy effects with simpler visuals (shadow → border)

6) Images: decode, resize, caching, and shimmer pitfalls

Images are a frequent source of UI and raster spikes due to decoding. A strong rule of thumb: don’t decode a 4K image to display a 100×100 thumbnail.

What to check

  • Are you fetching appropriate sizes from the backend (thumbnails)?
  • Do you use cacheWidth/cacheHeight with Image.network or ResizeImage?
  • Does your shimmer/placeholder repaint too much of the list?

Example: decode to target size

Image.network(
  url,
  width: 120,
  height: 120,
  fit: BoxFit.cover,
  cacheWidth: 240,   // e.g. 2x for high-DPI screens
  cacheHeight: 240,
)

7) Shader compilation jank (often on Android)

Sometimes the app stutters only the first time a specific effect or animation appears. That can be shader compilation.

  • Symptom: stutter on first occurrence, smooth afterwards.
  • Timeline: Skia/shader-related events around the stutter.

Depending on Flutter version and platform, options include:

  • upgrading Flutter (many engine-level improvements happen here)
  • warming up critical animations carefully (don’t hurt app startup)
  • avoiding extreme effects (complex masks, heavy blur)

8) GC and allocations: stutters every few seconds

If stutters are periodic and you see GC activity in the timeline, you’re likely allocating too much in hot paths (scrolling, animations).

Common sources of allocations

  • Creating new objects in build for every frame (lists, maps, regexes).
  • Formatting dates/numbers during scrolling without caching (e.g., intl).
  • Unnecessary string concatenation inside builders.

What to do

  • Cache computed values outside build.
  • Use const and move constants to static fields.
  • For expensive computations: use an isolate (next section).

9) CPU-heavy work: move it off the UI thread

The UI thread shouldn’t perform heavy work (big JSON parsing, compression, crypto, large sorts) during user interaction.

Minimal pattern: compute()

import 'package:flutter/foundation.dart';

Future<Result> parseBigJson(String raw) async {
  return compute(_parse, raw);
}

Result _parse(String raw) {
  return Result.fromJson(raw);
}

Note: isolates aren’t magic. Passing huge objects between isolates can be expensive; prefer passing raw bytes/strings and returning a small result.

10) Animations: reduce work per frame

Animations in Flutter can be cheap or expensive depending on what changes every frame. Relayouting large parts of the UI is a common cause of jank.

Animation checklist

  • Use implicit animations on small widgets (AnimatedOpacity, AnimatedContainer), but understand what they animate.
  • With AnimatedBuilder, pass a stable child so static parts don’t rebuild.
  • Avoid animating size changes in lists (can force relayout of many items).

11) Don’t do this: frequent performance traps

  • Calling setState in loops or at frame-rate without throttling.
  • IntrinsicHeight inside scrollable lists.
  • Large blur and full-screen masks.
  • Oversized images without resizing/caching.
  • Debug printing in hot paths (logs can cause stutters).

12) Anti-regression: keep performance stable

A one-time “performance fix” won’t last unless you protect it. A lightweight routine that works in practice:

  • Profile 1–2 critical flows at the end of each sprint (feed scroll, search, checkout).
  • Define a budget and a baseline device: e.g., “scroll 200 items with no red bars on Pixel X.”
  • Review UI changes with a performance lens: lists, images, effects.
  • Track crashes/ANRs and startup time metrics to catch regressions early.

Summary

Diagnosing Flutter jank is mostly process: measure in profile, separate UI vs raster, inspect the worst frames, and fix the root cause—rebuilds, layout/paint, images, shaders, GC, or CPU-heavy work. Treat this checklist as your playbook whenever smoothness becomes an issue.

FAQ

Does RepaintBoundary always help?

No. It helps when repaint work is the bottleneck and you can isolate frequently changing regions. If your issue is build or layout, it may not help and can add overhead.

Can you guarantee 60 FPS on every phone?

Not always. Hardware and refresh rate define the budget. Aim for consistent smoothness on your target device tier and avoid large spikes on older devices.

Can release be slower than debug?

It’s less common, but possible due to shader compilation behavior, GPU differences, or allocation patterns. That’s why you must test on real devices in profile/release modes.

Read also: The Best Examples of Apps Built with Flutter

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

seventeen − 12 =