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 Data

What 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

  1. Add const to every widget that doesn't change
  2. Use ListView.builder instead of ListView with children
  3. Replace Provider.of with Consumer or Selector
  4. Add cacheHeight to 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.