Flutter — Performance Optimization Tips
To be honest: every Flutter app feels silky smooth the very first time you run flutter run. A brand-new project, an empty widget, no network calls — everything feels clean and effortless. Then reality hits. You add some API calls, load 500 items into a ListView, put more content into each item, maybe throw in an animation or two… Suddenly, your silky Dove chocolate smoothness feels more like frozen butter straight out of the fridge.
Animations start lagging like it’s Windows 98, scrolling feels stickier than a toddler grabbing candy, and QA testers start filing bug reports left and right. The screenshots look like horror movies. Meanwhile, users leave one-star reviews: “The app is too slow. Please fix it, or just give up.”
The good news? Your app doesn’t have to feel like dial-up Internet Explorer. With the right techniques, you can get it flying again.
1. Stop Returning Widgets from Methods
To keep “clean code,” we often hide UI parts inside helper methods — buildButton(), getCard(), makeHeader(). It looks tidy in the editor, but under the hood Flutter doesn’t reuse these widgets. Each call creates a brand-new widget, forcing the framework to rebuild and re-layout unnecessarily. In other words, our code looks neat, but Flutter’s code becomes bloated.
Mistake ❌ — Returning widgets from methods
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
buildButton("Login"),
buildButton("Register"),
],
);
}
Widget buildButton(String text) {
return ElevatedButton(
onPressed: () {},
child: Text(text),
);
}
}Every time build() runs, Flutter creates a brand-new button — even if nothing changed. Smoothness gradually disappears.
Solution ✅ — Extract into separate widgets
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: const [
ActionButton(text: "Login"),
ActionButton(text: "Register"),
],
);
}
}
class ActionButton extends StatelessWidget {
final String text;
const ActionButton({required this.text, super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(text),
);
}
}Now Flutter can optimize and reuse widgets since they’re split into independent components. No unnecessary rebuilds, smoother performance.
2. Keep build() Lightweight
A common mistake is treating Flutter’s build() like a junk drawer — dumping everything inside.
Parsing JSON? Toss it in. Filtering lists? Why not. Formatting dates? Sure, let’s make Flutter suffer more. Some even make network calls inside build(), like cooking every time someone opens the fridge. It works… until Flutter has to rebuild, and suddenly your “smooth app” feels like dragging a piano uphill.
Problem ❌ — Doing work in build()
class ProductList extends StatelessWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
Widget build(BuildContext context) {
// Filtering inside build()
final discounted = products.where((p) => p.isDiscounted).toList();
return ListView.builder(
itemCount: discounted.length,
itemBuilder: (context, index) {
return Text(discounted[index].name);
},
);
}
}Every rebuild repeats the .where() filter — on large lists, this wastes time and causes lag.
Solution ✅ — Precompute outside build()
class ProductList extends StatefulWidget {
final List<Product> products;
const ProductList({super.key, required this.products});
@override
State<ProductList> createState() => _ProductListState();
}
class _ProductListState extends State<ProductList> {
late List<Product> discounted;
@override
void initState() {
super.initState();
// Precompute once
discounted = widget.products.where((p) => p.isDiscounted).toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: discounted.length,
itemBuilder: (context, index) {
return Text(discounted[index].name);
},
);
}
}Now build() only arranges widgets. No extra computation. Cleaner, faster, and easier to maintain.
3. Use Isolates for Heavy Tasks
Flutter has only one UI thread. If you hand it heavy work — parsing giant JSON, crunching numbers, resizing images — it chokes. The result? Your app stutters worse than a video call over hotel Wi-Fi.
The fix? Offload heavy tasks to an isolate. Think of them as Flutter doing the work in another room so your UI doesn’t suffocate.
Get Slime’s stories in your inbox
Join Medium for free to get updates from this writer.
Problem ❌ — JSON parsing freezes the UI
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
class JsonFreezeExample extends StatefulWidget {
const JsonFreezeExample({super.key});
@override
State<JsonFreezeExample> createState() => _JsonFreezeExampleState();
}
class _JsonFreezeExampleState extends State<JsonFreezeExample> {
String result = "Tap to load JSON";
Future<void> loadJson() async {
final raw = await rootBundle.loadString('assets/large_file.json');
//This parsing is synchronous and heavy!
final data = jsonDecode(raw);
setState(() {
result = "Loaded ${data.length} items";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("JSON Freeze Demo")),
body: Center(child: Text(result)),
floatingActionButton: FloatingActionButton(
onPressed: loadJson,
child: const Icon(Icons.download),
),
);
}
}If large_file.json has thousands of records, parsing blocks the UI completely — no animations, no taps, nothing until parsing finishes.
Solution ✅ — Parse JSON in an isolate (compute)
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
// Top-level function required for compute()
dynamic parseJson(String raw) {
return jsonDecode(raw);
}
class JsonIsolateExample extends StatefulWidget {
const JsonIsolateExample({super.key});
@override
State<JsonIsolateExample> createState() => _JsonIsolateExampleState();
}
class _JsonIsolateExampleState extends State<JsonIsolateExample> {
String result = "Tap to load JSON";
Future<void> loadJson() async {
final raw = await rootBundle.loadString('assets/large_file.json');
// Offload heavy parsing to another isolate
final data = await compute(parseJson, raw);
setState(() {
result = "Loaded ${data.length} items";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("JSON Isolate Demo")),
body: Center(child: Text(result)),
floatingActionButton: FloatingActionButton(
onPressed: loadJson,
child: const Icon(Icons.download),
),
);
}
}4. The Widget Onion
We’ve all seen (or written) it :
Container > Padding > Container > SizedBox > Padding > Container.
Before you know it, your widget tree looks like an onion — and like an onion, it makes people cry.
In reality, most of these wrappers don’t do anything but slow down rebuilds and make code harder to read. Flutter already provides ways to combine padding, decoration, and layout into fewer widgets — your widget tree (and your teammates) will thank you.
Problem ❌ — The “onion” widget tree
class BadNesting extends StatelessWidget {
const BadNesting({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container( // unnecessary
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16),
child: Container( // another one
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
"Too... many... wrappers...",
style: const TextStyle(color: Colors.white),
),
),
),
),
),
),
);
}
}Multiple rebuild layers that add no real value — Flutter still has to walk through and build them all. It’s like wrapping a sandwich in 5 layers of foil and then asking why it takes so long to unwrap.
Solution ✅ — Use built-in properties whenever possible
class GoodNesting extends StatelessWidget {
const GoodNesting({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
padding: const EdgeInsets.all(16), // combine padding here
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
"Much cleaner ✨",
style: TextStyle(color: Colors.white),
),
),
),
);
}
}Same result, but with half the widget tree. Cleaner code, lighter builds, easier debugging.
5. Zombie Listeners You Forgot to Kill
Memory leaks in Flutter usually happen when you forget to clean up controllers, streams, or listeners. At first, everything seems fine. But after a few navigations or scrolls, your app starts hoarding memory like a caffeine-powered dragon. Soon enough, scrolling stutters, animations lag, and on older phones it may even crash.
Problem ❌ — Zombie listeners
class BadLeak extends StatefulWidget {
const BadLeak({super.key});
@override
State<BadLeak> createState() => _BadLeakState();
}
class _BadLeakState extends State<BadLeak> {
final _controller = PageController();
@override
Widget build(BuildContext context) {
return PageView(
controller: _controller,
children: const [
Text("Page 1"),
Text("Page 2"),
],
);
}
}Here, _controller is never disposed. Leave the page a few times, and congrats — you’ve created a leak.
Solution ✅ — Always dispose properly
class GoodLeak extends StatefulWidget {
const GoodLeak({super.key});
@override
State<GoodLeak> createState() => _GoodLeakState();
}
class _GoodLeakState extends State<GoodLeak> {
final _controller = PageController();
@override
void dispose() {
_controller.dispose(); // Clean up
super.dispose();
}
@override
Widget build(BuildContext context) {
return PageView(
controller: _controller,
children: const [
Text("Page 1"),
Text("Page 2"),
],
);
}
}Cleanup is like doing the dishes — nobody enjoys it, but skip it and things go bad quickly.
All these little things add up — forgotten listeners, nested containers, heavyweight build() methods. As developers, we tend to shrug them off (“Eh, it’s just one setState(). What’s the worst that could happen?”).
But like skipping workouts, leaving dishes undone, or watching “just one more episode” at 2 AM, these small lapses accumulate fast. Alone, they’re harmless. Together, they drag your app’s performance down faster than dial-up Internet Explorer.
So clean up your code, keep it lean, and remember: Flutter is fast — most of the time, it’s us slowing it down.






