skip to content

Jank & Size Tuning

Making a Flutter app fast and small: rebuild scope, reading frame timings, raster-thread cost, isolate offloading, and binary size and startup. Interviewers ask how you would find a dropped frame.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

26

In Flutter, what does the compute() function do, and how would you use it to parse a large JSON file without freezing the UI?

level: juniorimportance: must knowfreq 66%

answer

  1. one isolate runs build, layout and your Dart
  2. package:flutter/foundation.dart
  3. a callback plus one message
  4. returns a Future of the result
  5. load the asset first, parse inside

basics

~20 s

compute(callback, message) runs callback(message) on a new background isolate and returns a Future with the result, so heavy CPU work such as jsonDecode plus model mapping no longer blocks the main isolate that builds and schedules frames.

solid answer

~40 s

All of a Flutter app's Dart code, including `build`, layout and your own logic, runs on the **main isolate** by default, so a long synchronous job like decoding a 20 MB JSON file stalls frames. `compute()` from `package:flutter/foundation.dart` takes a callback and a single message, runs `callback(message)` on a freshly spawned isolate and returns a `Future` that completes with the callback's result. You read the file on the main isolate (for example with `rootBundle.loadString`, which is not available in spawned isolates), then `await compute(parseDepartures, body)` where `parseDepartures` is a top-level function doing the decode and the mapping into models. On Android, iOS and desktop it is equivalent to `Isolate.run(() => callback(message))`; while it runs, the UI keeps animating.

code

dart · 29 lines
dart
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class Departure {
  const Departure({required this.stopId, required this.time});

  factory Departure.fromJson(Map<String, Object?> json) => Departure(
        stopId: json['stopId']! as String,
        time: json['time']! as String,
      );

  final String stopId;
  final String time;
}

// Top-level: runs on the spawned isolate and captures nothing.
List<Departure> parseDepartures(String body) {
  final List<Object?> raw = jsonDecode(body) as List<Object?>;
  return raw.cast<Map<String, Object?>>().map(Departure.fromJson).toList();
}

Future<List<Departure>> loadSchedule() async {
  // Asset I/O stays on the main isolate.
  final String body = await rootBundle.loadString('assets/schedule.json');
  // The CPU-heavy decode and mapping run on a background isolate.
  return compute(parseDepartures, body, debugLabel: 'parseDepartures');
}

go deeper

for a junior

Know that long synchronous Dart work freezes the UI and that compute(callback, message) moves it to another isolate and returns a Future.

for a middle

Explain what stays on the main isolate (asset loading, setState) and what goes into the callback, and why the callback is top-level.

for a senior

Decide from profiling whether the work is long enough to justify an isolate, and structure the result so it is cheap to hand back.

for a principal

Set a team rule for where parsing and heavy transforms live so screens do not rediscover jank one by one.

## Why heavy parsing freezes a Flutter app A Flutter app runs its Dart code on one **isolate**, the **main isolate**, by default. An isolate is Dart's unit of concurrency: it has its own memory and its own event loop, and it runs one piece of Dart code at a time. On the main isolate that code includes the framework's work for every frame (building widgets, layout, painting instructions) as well as your app logic. If a single synchronous job, such as `jsonDecode` on a 20 MB transit-schedule file followed by turning every entry into a model object, takes several hundred milliseconds, nothing else on the main isolate can run in that time. No frame is produced, animations stop and taps queue up. Marking the function `async` does not change that: the decoding itself is still one long synchronous stretch on the same isolate. ## What compute() does `compute` lives in `package:flutter/foundation.dart`: - **Signature:** `Future<R> compute<M, R>(ComputeCallback<M, R> callback, M message, {String? debugLabel})`, where the callback has the shape `FutureOr<R> Function(M message)`. - **On Android, iOS and desktop** it spawns a new isolate, runs `callback(message)` there, sends the result back and shuts the isolate down. The docs state it is equivalent to `await Isolate.run(() => callback(message))`. - **On the web** it runs the callback on the same event loop, because Dart on the web has no isolates (a separate topic, but worth knowing before relying on it). - **`debugLabel`** names the spawned isolate, so its timeline events are easy to find while profiling. The call returns immediately with a `Future`; the main isolate keeps producing frames while the other isolate works, and the `await` resumes once the result arrives. ## Using it for a large JSON file 1. **Load on the main isolate.** Asset loading through `rootBundle` is tied to the main isolate, so read the file there. Reading is asynchronous I/O and does not block frames. 2. **Put all CPU-heavy steps in one callback.** Decoding, casting and mapping to model classes all belong inside the function you hand to `compute`. 3. **Make the callback a top-level or static function.** It then carries no hidden state to the new isolate, only the message. 4. **Return plain data.** A list of model objects is fine; objects such as a `Future` or an HTTP response object are not good return values. 5. **Update the UI after the await**, checking `mounted` first if you are inside a `State`. ## What stays on the main isolate | Step | Where it runs | |---|---| | `rootBundle.loadString(...)` | main isolate (async I/O) | | `jsonDecode` and `fromJson` mapping | the `compute` isolate | | Showing a progress indicator | main isolate, keeps animating | | `setState` with the parsed list | main isolate | ## Common mistakes - Wrapping the work in `Future(() => ...)` or `async` and expecting it to move off the main isolate. - Calling `compute` for tiny jobs where spawning costs more than the work. - Trying to touch widgets, `BuildContext` or `rootBundle` inside the callback: all UI work belongs to the main isolate.

  • In Flutter, what happens if the compute() callback throws?
    The exception is carried back and the `Future` returned by `compute` completes with that error, so you handle it with `try`/`catch` around the `await` on the main isolate like any other asynchronous failure. The spawned isolate is gone either way.
  • Why can't the compute() callback call rootBundle.loadString itself?
    Flutter's asset bundle and all UI services are tied to the main isolate; the Flutter docs state you cannot access assets through `rootBundle` in spawned isolates. Load the string first, then pass it as the message.

The main isolate is a chef who must also plate every dish on time; compute() hires a prep cook in a separate kitchen, hands over the raw ingredients through a hatch and gets back chopped vegetables, so plating never stops.

saying these in an interview costs you the question

  • Marking the parse function async moves it off the main isolate.
  • compute() runs the callback on a thread that shares the main isolate's memory.
  • You can call setState or read BuildContext inside the compute() callback.
  • compute() blocks the caller until the result is ready.
  • compute() creates a background isolate on the web too.
open as a page

In Flutter, why does a const widget inside a build method skip rebuilding when its parent rebuilds, and when does it still rebuild?

level: juniorimportance: must knowfreq 70%

basics

~10 s

A const constructor call yields the same canonical instance on every build, and Element.updateChild leaves an identical child untouched. Its subtree is skipped unless an inherited dependency or its own state marks it dirty.

open as a page

In Flutter, how do you measure what makes up a release app's size, and why are debug builds and upload packages misleading?

level: middleimportance: must knowfreq 48%

basics

~20 s

Build a release with --analyze-size (one ABI on Android), read the terminal summary, then open the generated code-size-analysis JSON in the DevTools app size tool. Debug builds carry JIT overhead, and upload packages hold content the stores strip per device.

open as a page

In Flutter DevTools' frame chart, what do the UI and raster bars of a frame measure, and how do you tell which one made a frame janky?

level: middleimportance: must knowfreq 60%

basics

~20 s

Each frame has a UI bar (Dart work) and a raster bar (the engine drawing the scene). Each must fit the budget, about 16 ms at 60 Hz or 8 ms at 120 Hz; the bar over it points to the cause.

open as a page

In Flutter, why is wrapping a subtree in an Opacity widget with a value like 0.5 costly, and what cheaper options exist?

level: middleimportance: must knowfreq 62%

basics

~20 s

A fractional Opacity makes Flutter paint the child into an offscreen buffer (a saveLayer) and composite it back, which costs raster time. Cheaper: put the alpha in the color, use Image's color or opacity parameters, or Visibility for on/off.

open as a page

A Flutter stopwatch page calls setState in its top-level State every 100 ms, rebuilding the whole Scaffold; how do you shrink each rebuild to the elapsed-time text?

level: middleimportance: must knowfreq 55%

basics

~20 s

setState rebuilds the whole subtree of the State that calls it. Move the timer and elapsed value into a small StatefulWidget that renders only the time text, or into a ValueNotifier read by one builder, and make the static rest const.

open as a page

In Flutter, how do you measure the time to first frame with flutter run --trace-startup, and what does it report?

level: juniorimportance: should knowfreq 26%

basics

~20 s

Run flutter run --profile --trace-startup: the tool records startup, prints 'Time to first frame', writes start_up_info.json to the build directory and exits. It measures from the engine's entry point to the first frame built and rasterized.

open as a page

How do you turn on Flutter's PerformanceOverlay on a running app, and what do its two graphs tell you?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Set showPerformanceOverlay: true on MaterialApp, press P in a flutter run session, or use DevTools' Performance Overlay button. The engine draws two graphs over the app: raster time on top and UI time below, with red bars for frames that missed the budget.

open as a page

In Flutter, how do you spot images decoded at far more pixels than they are displayed, and why do they hurt performance?

level: juniorimportance: should knowfreq 32%

basics

~20 s

Turn on DevTools' Highlight oversized images (debugInvertOversizedImages): images decoded at least 128 KB bigger than needed are drawn inverted and flipped, with a console message. They waste memory, crowd the image cache and make every draw sample a huge texture.

open as a page

How can a Flutter app measure its frame times in release builds with SchedulerBinding.addTimingsCallback, and what do FrameTiming's durations mean?

level: middleimportance: should knowfreq 28%

basics

~10 s

SchedulerBinding.instance.addTimingsCallback receives batches of FrameTiming objects, even in release builds. buildDuration is UI-thread time, rasterDuration is raster-thread time, and totalSpan runs from vsync to raster finish. Compare each against the frame budget.

open as a page

In a Flutter web build, what does compute() actually do, and why can a heavy parse still freeze the page?

level: middleimportance: should knowfreq 30%

basics

~20 s

On the web, compute() yields once and then runs the callback directly on the same event loop, because Dart web apps have no isolates. The API stays portable, but a long parse still blocks the page.

open as a page

In Flutter, when does moving a task into compute() fail to pay off, given the cost of spawning an isolate and moving data?

level: middleimportance: should knowfreq 44%

basics

~20 s

compute() spawns and tears down a new isolate on every call and moves the message and result across, so it only pays off for CPU work longer than a few milliseconds. Tiny, per-item or I/O-bound tasks gain nothing or get slower.

open as a page

In Flutter, why is compute() usually handed a top-level or static function rather than a closure or an instance method of a State?

level: middleimportance: should knowfreq 42%

basics

~20 s

compute() sends the callback itself to the new isolate along with the message. A top-level or static function captures nothing, while a closure or instance-method tear-off drags its captured context, often the whole State, along, which is costly or fails to send.

open as a page

In Flutter, what does animating an Opacity widget by calling setState on every tick cost, compared with an animated fade widget?

level: middleimportance: should knowfreq 52%

basics

~20 s

setState per tick reruns build for that State's whole subtree every frame. FadeTransition or AnimatedOpacity update the render object's opacity with no rebuild. Both still pay the offscreen compositing cost while the value sits between 0 and 1.

open as a page

In Flutter, what does passing a prebuilt child to a builder widget such as ValueListenableBuilder save, and when does the trick stop helping?

level: middleimportance: should knowfreq 40%

basics

~20 s

The child is built once, outside the builder callback, and handed back unchanged on every notification, so the framework skips rebuilding it. It fails when that subtree needs the value, or when the enclosing widget rebuilds and recreates it.

open as a page

A Flutter app bundle grew past 60 MB right after adding a charts package; how do you find exactly what grew and decide what to cut?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Build release size files before and after the change with the same flags and ABI, load both into the DevTools Diff tab, see whether Dart code, assets or native libraries grew, trace the cause with the dominator tree, then trim, replace or defer it.

open as a page

A Flutter app keeps its launch screen up for three seconds before the first frame; how do you find and cut what delays it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Trace startup in profile mode; if most time falls after framework init, the delay is your code. Profile the AppStartUp user tag, then shrink what main() awaits before runApp, run it in parallel, and postpone non-critical setup until after the first frame.

open as a page

Opening a Flutter photo-editor screen drops frames for the first second; how do you use DevTools' timeline, Track Layouts and Track Paints to find what is slow?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Record opening the screen in profile mode, select the red frames, and check which bar is long. For UI time, read the BUILD, LAYOUT and PAINT events, and turn on Track Layouts and Track Paints to name the costly render objects.

open as a page

A Flutter app freezes for about a second after loading a 20 MB transit-schedule file; how do you confirm main-isolate CPU work is the cause and fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Profile on a device: long UI bars with short raster bars, plus a CPU profile dominated by jsonDecode and fromJson on the main isolate, confirm it. Move decode, mapping and indexing into one compute() call, keep the file read outside, and re-profile.

open as a page

A Flutter screen with a BackdropFilter frosted-glass header stutters while its list scrolls underneath; how do you diagnose and cut that raster cost?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Profile on a real device: a high raster bar with a low UI bar points at the blur. Bound it with a ClipRect, shrink area and sigma, share one backdrop via BackdropGroup, and use ImageFiltered when only one child needs blurring.

open as a page

In Flutter, where does adding a RepaintBoundary actually help, and when does an extra one make performance worse?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A RepaintBoundary helps around a small, frequently repainting widget inside a large static area, or around a complex static subtree next to animation. Each boundary adds a layer and memory, so boundaries around things that always repaint together only add cost.

open as a page

In Flutter, how do you find which widgets rebuild too often on a janky screen, and what do debugProfileBuildsEnabled and DevTools' Track Widget Builds show?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Run in profile mode and enable Track Widget Builds in DevTools, which sets debugProfileBuildsEnabled and adds a timeline event for each widget built. Read which widgets build per frame, not the timings, then narrow the rebuild at its source.

open as a page

In Flutter, which Clip value triggers a saveLayer, how do the other Clip values compare in cost, and what do most widgets default to?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

Only Clip.antiAliasWithSaveLayer adds a saveLayer; it is very slow. Clip.hardEdge is the fastest real clip, Clip.antiAlias is smoother and a bit slower, and Clip.none, the default for most widgets, costs nothing.

open as a page

In Flutter on Android, how do deferred components with loadLibrary() reduce the initial download, and what setup do they need?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

Deferred components split Dart code and assets into Android dynamic feature modules that download at runtime when loadLibrary() is called. They need deferred imports, a deferred-components section in pubspec.yaml, Play Store split support and flutter build appbundle.

open as a page

In Flutter, why does a plugin call fail inside a background isolate by default, and how do RootIsolateToken and BackgroundIsolateBinaryMessenger fix it?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Platform channels need a messenger tied to the root isolate, which a spawned isolate lacks. Pass RootIsolateToken.instance from the root isolate and call BackgroundIsolateBinaryMessenger.ensureInitialized(token) in the background isolate; channels then work for request/response calls.

open as a page

How would you set and enforce a frame-time budget for a Flutter team's app that ships to both 60 Hz and 120 Hz devices?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Express the budget per thread as 1000 divided by the refresh rate with headroom, so about 16 ms or 8 ms. Check it in profile mode on reference devices, watch the field with FrameTiming percentiles, and gate on regressions, not single frames.

open as a page