Flutter / Inclusive interfaces and target evidence / Lesson 15
Profiling, isolates, and rendering cost
Locate the expensive phase before changing code and isolate CPU work only when its transfer costs are justified.
On this page
Lesson goals
- Measure the interaction on a representative target
- Distinguish build, layout, and raster work
- Avoid repeated pure work before adding concurrency
Measure the interaction on a representative target
A performance report needs a concrete interaction, representative data size, target hardware, build mode, and observed metric. Debug mode adds instrumentation and is not a reliable release-performance baseline. Use the supported profile tooling for the intended target and inspect repeated behavior, not only one unusually fast frame. Different display refresh rates also imply different frame deadlines, so a single universal frame budget is misleading. Record whether users experience delayed input, slow initial loading, scrolling jank, or excessive memory; these symptoms can have different causes and should not all be called a slow app.
Distinguish build, layout, and raster work
A long Dart task can prevent timely processing of UI events. A complex layout can consume time despite simple application logic. Rasterization can be expensive because of large images, broad effects, clipping, or offscreen layers. Use tracing to locate the phase rather than treating every visible hitch as evidence that widgets rebuild too often. Adding RepaintBoundary everywhere can increase resource costs and does not fix synchronous JSON decoding. Likewise, moving a calculation to another isolate does not reduce an unnecessarily expensive visual effect. Match the intervention to measured evidence.
Avoid repeated pure work before adding concurrency
The first fragment computes a filtered list once from the current input snapshot. In a widget owner, derive it when the relevant input changes or use a justified cache keyed by all dependencies. Do not cache only by list length, since contents can change without a new length. Avoid prematurely retaining large intermediate lists indefinitely. Sometimes simplifying an algorithm or moving a calculation out of a per-row callback is enough. Establish correctness on representative empty, small, and large inputs before deciding that concurrency is the necessary solution.
List<int> visibleScores(List<int> scores, int minimum) =>
scores.where((score) => score >= minimum).toList(growable: false);
void main() {
print(visibleScores([3, 8, 5, 9], 6));
print(visibleScores([], 6));
}
[8, 9]
[]
Filtering is linear in input size and preserves input order. This tiny example does not justify an isolate or a performance claim.
Use isolates with an explicit message contract
Dart isolates have separate memory and communicate through supported messages rather than sharing ordinary mutable state. On supported native Dart targets, compute can run an eligible callback away from the UI isolate; web behavior differs and does not provide the same parallel execution guarantee. The second fragment keeps the input and output simple. Isolate startup, serialization or copying, and result delivery have costs, so moving a tiny task can be slower overall. Avoid capturing a large object graph or trying to send BuildContext, controllers, or arbitrary platform resources as work input.
import 'package:flutter/foundation.dart';
int sumSquares(List<int> values) =>
values.fold<int>(0, (total, value) => total + value * value);
Future<int> calculate() => compute(sumSquares, <int>[2, 3, 4]);
// await calculate() -> 29
The calculation returns 29; execution placement depends on the target.
This illustrative callback uses sendable primitive collection data. It does not claim measured speedup or native execution on web.
Manage images and memory as a separate budget
A compressed image file can decode into a much larger pixel buffer. Displaying many full-resolution images as tiny thumbnails may waste memory and decoding time. Review appropriate decode dimensions, cache policy, and the target’s image pipeline rather than blaming only widget count. Retained controllers, subscriptions, and lists can also keep data alive after screens close. Observe memory across repeated open-and-close cycles to distinguish a stable cache plateau from ongoing growth. Do not prescribe arbitrary cache limits without measuring the application’s content and target constraints.
Compare before and after with the same workload
Change one suspected cause, rerun the same interaction, and compare traces and correctness. Keep data, target, build mode, and instrumentation consistent. A faster average can conceal a worse tail latency or a new memory cost, so inspect the metrics relevant to the user task. Preserve tests for ordering, cancellation, and stale results when introducing asynchronous computation. An isolate result can still arrive after its owner or input becomes obsolete. Report improvements only from actual measurements; this course’s small arithmetic outputs are correctness illustrations and provide no device benchmark.
Common errors and fixes
Awaiting a CPU-heavy function still freezes input
- Why it happens
- async did not move synchronous work off the UI isolate.
- How to fix it
- Measure and simplify the task, then isolate suitable work when justified.
- Prevent it next time
- Distinguish scheduling from parallel execution.
A cache returns stale filtered data
- Why it happens
- The cache key omitted a dependency such as threshold or contents.
- How to fix it
- Key by all relevant immutable inputs or invalidate under one owner.
- Prevent it next time
- Test same-length data changes and changed filters.
A native isolate claim is applied to web
- Why it happens
- Target-specific compute behavior was ignored.
- How to fix it
- Read and state the supported target behavior explicitly.
- Prevent it next time
- Keep measurements and claims tied to the actual target.
Check your understanding
1. Why is moving a four-number sum to compute unlikely to be a useful optimization?
Show worked solution
The calculation is tiny compared with scheduling and message costs. The example teaches the boundary, not a speedup. Measure representative large work first, simplify unnecessary computation, and compare total user-visible latency before adopting concurrency.
2. A scroll hitch shows raster work rising while Dart work stays low. What should be investigated?
Show worked solution
Inspect images, expensive effects, clipping, offscreen layers, and repaint scope in the measured trace. Rewriting a pure filter or adding isolates is unlikely to address that phase. Change one visual cause and compare the same scrolling workload.
Key takeaways
- Measure the interaction on a representative target
- Compare before and after with the same workload