After one weekend of optimization, I got it down to 1.4 seconds.
Here's every single thing I did.
The Starting Point Was Bad
I used the Flutter DevTools performance tab and found some embarrassing stuff:
- Home screen was rebuilding 6 times on startup
- I was loading 50 images at once
- My list had 200 items rendering immediately
- Database queries running on the main thread
Yeah. Not great.
Fix 1: Stop Rebuilding Everything
I had this in my home screen:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final users = Provider.of<UserData>(context).users;
final posts = Provider.of<PostData>(context).posts;
final settings = Provider.of<Settings>(context).config;
return Column(
children: [
UserList(users: users),
PostFeed(posts: posts),
],
);
}
}Every time anything changed anywhere, the whole screen rebuilt. I changed it to:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Consumer<UserData>(
builder: (context, userData, child) {
return UserList(users: userData.users);
},
),
Consumer<PostData>(
builder: (context, postData, child) {
return PostFeed(posts: postData.posts);
},
),
],
);
}
}Now only the parts that actually changed would rebuild.
Result: Saved 800ms on initial load.
Fix 2: Lazy Load Images
I was doing this:
ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return Image.network(posts[index].imageUrl);
},
)All 50 images tried to load at once. The network was crying.
Changed to:
ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return Image.network(
posts[index].imageUrl,
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return Container(
height: 200,
child: Center(child: CircularProgressIndicator()),
);
},
cacheHeight: 400,
);
},
)The cacheHeight parameter tells Flutter to resize images before caching. Saved a ton of memory.
Result: Saved 1.2 seconds.
Fix 3: Use ListView.builder Properly
I had this rookie mistake:
Widget build(BuildContext context) {
return ListView(
children: posts.map((post) => PostCard(post: post)).toList(),
);
}This creates ALL widgets immediately. Even the ones you can't see.
Fixed version:
Widget build(BuildContext context) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return PostCard(post: posts[index]);
},
);
}Now it only builds what's visible on screen.
Result: Saved 600ms.
Fix 4: Move Heavy Work Off Main Thread
I was loading data from SQLite right in the build method:
Future<void> loadData() async {
final db = await database;
final posts = await db.query('posts');
setState(() {
this.posts = posts;
});
}This blocked the UI while the database read happened.
I moved it to an isolate for the initial load:
Future<List<Post>> loadDataInBackground() async {
return await compute(fetchPostsFromDb, null);
}
static Future<List<Post>> fetchPostsFromDb(_) async {
final db = await openDatabase('app.db');
final results = await db.query('posts');
return results.map((r) => Post.fromMap(r)).toList();
}The UI stays smooth while data loads in the background.
Result: Saved 400ms.
Fix 5: Cache Network Responses
I was fetching the same API data on every app start:
Future<void> loadPosts() async {
final response = await http.get(Uri.parse('api/posts'));
final posts = jsonDecode(response.body);
setState(() {
this.posts = posts;
});
}Added simple caching with shared_preferences:
Future<void> loadPosts() async {
final prefs = await SharedPreferences.getInstance();
final cached = prefs.getString('posts_cache');
if (cached != null) {
setState(() {
this.posts = jsonDecode(cached);
});
}
final response = await http.get(Uri.parse('api/posts'));
await prefs.setString('posts_cache', response.body);
setState(() {
this.posts = jsonDecode(response.body);
});
}Show cached data instantly, then update with fresh data.
Result: Initial load now instant with cache.
Fix 6: Const Widgets Everywhere
This is embarrassing but I wasn't using const:
Container(
padding: EdgeInsets.all(16),
child: Text('Hello'),
)Changed to:
Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
)When widgets are const, Flutter doesn't rebuild them. Ever. Big deal for things that never change.
Result: Saved 200ms and reduced jank.
The Load Flow Now
App Start
|
v
Load Cached Data ──> Show UI (300ms)
|
v
Start Background Tasks
|
|──> Fetch Fresh API Data
|
|──> Load Database (isolate)
|
v
Update UI with Fresh DataWhat I Measured
Before:
- Cold start: 4.2 seconds
- Time to interactive: 5.1 seconds
- Frame drops: 23 in first 5 seconds
After:
- Cold start: 1.4 seconds
- Time to interactive: 1.8 seconds
- Frame drops: 2 in first 5 seconds
That's 3x faster on the metric that matters most.
Should You Do This?
If your app feels slow, yeah. Start with DevTools. Record a timeline of your app starting up. You'll find the problems.
Most Flutter performance issues come from:
- Building too much at once
- Not using const
- Loading everything upfront
- Blocking the main thread
Fix those four and you'll get most of the gains.
Quick Wins You Can Do Today
- Add
constto every widget that doesn't change - Use
ListView.builderinstead ofListViewwith children - Replace
Provider.ofwithConsumerorSelector - Add
cacheHeightto your images
That's it. Nothing magical. Just stop doing things that Flutter's screaming at you not to do.
My 4-second load time was because I ignored the basics. Once I fixed them, everything got fast.