A scrolling list that drops to 35fps on a budget Android is not "the platform being slow," it is a stack of small mistakes you can find with the tools shipped in the Flutter SDK. This lab gives you a deliberately slow list, walks you through three diagnostic tools (DevTools timeline, the performance overlay, and a small custom frame logger), and applies four fixes that bring it to a steady 58fps on a Snapdragon 4-series device.
Profiling lists is one chapter. The full course covers offline storage, sync engines, and the architecture decisions that make those budget Androids feel native.
Open the starter project and run it in profile mode on a budget Android (Snapdragon 4-series, 4GB RAM, mid-tier OEM skin). The home screen renders a list of 5,000 items, each with an image, two text rows, and a trailing chevron. The list scrolls at roughly 35fps with periodic dropped frames into the low 20s. Our goal is a steady 58fps.
If you only have an emulator or a high-end phone, the numbers will look different but the shape of the fixes is identical. Profile mode is non-negotiable: debug-mode overhead lies about everything.
You will see two graphs at the top of the screen. The top one is the GPU (raster) timeline, the bottom is the UI (build + layout + paint) timeline. The horizontal bar on each is 16.6ms. Anything that crosses the bar is a dropped frame.
In our starter project, the UI bar is consistently spilling over. That tells us the work is in build(), layout(), or paint(). Not the GPU.
Open Flutter DevTools and switch to the Performance tab. Hit Record, scroll the list for two seconds, hit Stop. The flame graph shows you which methods are eating the frame budget.
For our starter list, the largest bar is _HomeListItem.build at ~9ms per frame. There are 14 items visible, so that is 14 × 9ms = 126ms of build per frame, way over budget. The cost is in three places:
A Text widget that does its own date formatting on every build.
A MemoryImage that re-decodes the same bytes on every scroll.
A Padding widget that wraps a Padding widget that wraps a Padding widget. (Yes, really. Production code.)
This drops the per-build cost of formatting from ~0.4ms to zero on hot scrolls. Across the visible viewport, that is ~5ms per frame back.
Fix 2: cache decoded images.
MemoryImage does not cache by default; it allocates a fresh ImageStream on every build of the same byte payload. The cheapest fix is to lift the Image widget into a const or to switch to Image.memory(... cacheWidth: 96) so the framework caches by (bytes, cacheWidth). For network images, use cached_network_image.
This is the largest single win in the lab: ~3ms per visible item back.
Fix 3: collapse nested padding.
Three nested Padding widgets create three layout passes. Collapse them into one EdgeInsets.fromLTRB(...) on the outermost container. This sounds trivial; the layout tree depth in our starter list is so deep that flattening it cuts ~1.5ms per item.
Fix 4: extract the static parts to const.
The trailing chevron is a static Icon(Icons.chevron_right). Marking it const excludes it from the rebuild tree entirely. Same for any divider, spacer, or text label whose value never changes.
Re-record the timeline after the fixes. The flame graph for _HomeListItem.build should now sit around 1.5ms. Across 14 visible items, that is 21ms total, comfortably within the 16.6ms budget per frame because the 14 items are not all rebuilt every frame; only the ones that scroll into view.
The performance overlay should show both bars holding steady under the line. Your scroll feel goes from "rough" to "is this the same app?" at exactly the right pixel.
Any of the four fixes alone gets you a 2–4fps improvement. All four together get you the 23fps you needed. The discipline trap is to find one obvious culprit, ship the fix, declare victory, and never measure the rest. Performance work is multiplicative: small fixes compound. Single fixes rarely move the needle alone.
The same is true for sync-engine work, for backend latency, for cold-start budgets. The full Offline-first Flutter course teaches the systemic version of this: how to set frame-budget alerts in CI, how to keep a perf log for the team, and how to argue against features that would push you back over the line. This lab is the first muscle. The course is the workout plan.