← Back to Portfolio Muhammad Shafeeq
Firebase

How I Cut Firestore Latency by 35% — Tricks That Actually Work

Server infrastructure representing Firebase performance

When I joined Flutter Studio, one of the first tasks I was handed was a Flutter app that "felt slow". The data was loading, but there was a noticeable delay every time the user navigated between screens. After a week of profiling and testing, I shaved 35% off the average data sync latency. Here's exactly what I did.

Problem 1: Fetching Too Much Data

The original code fetched entire collections on every screen load:

// ❌ Don't do this
final snapshot = await firestore.collection('appointments').get();

On a collection with 2,000 documents, this was expensive. The fix: query only what you need, always.

// ✅ Filter at the source
final snapshot = await firestore
  .collection('appointments')
  .where('doctorId', isEqualTo: currentDoctorId)
  .where('status', isEqualTo: 'pending')
  .orderBy('date', descending: true)
  .limit(20)
  .get();

The .limit(20) alone dropped response time significantly on the first load. Combine it with proper .where() filters and you're only moving bytes that matter.

Problem 2: Missing Composite Indexes

When you add multiple .where() clauses or combine .where() with .orderBy(), Firestore requires a composite index. Without it, the query runs slowly (or not at all).

The fastest way to find missing indexes: run the app in debug mode, execute the query, and Firestore will throw an error with a direct link to create the index in the console. Click it, wait two minutes, done.

For production, I now document all composite indexes in a firestore.indexes.json file and deploy them with the Firebase CLI:

firebase deploy --only firestore:indexes

Problem 3: Listening When You Should Be Getting

We had real-time listeners (.snapshots()) on data that only changed once a day — appointment history, doctor profiles. Every time any document in those collections changed, every connected client received an update. Wasteful.

// ❌ Real-time listener on static-ish data
firestore.collection('doctors').snapshots()

// ✅ One-time fetch for data that doesn't change often
firestore.collection('doctors').get(
  const GetOptions(source: Source.serverAndCache),
)

Use .snapshots() only for data that users genuinely need to see update live — chat messages, order status, appointment queues. For everything else, .get().

Problem 4: No Offline Persistence

Firebase's offline persistence is enabled by default on mobile, but I found it was being accidentally disabled in our initialization code. Turning it back on meant returning users saw data instantly from cache while a background sync happened:

await Firebase.initializeApp();
FirebaseFirestore.instance.settings = const Settings(
  persistenceEnabled: true,
  cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);

This is the single highest-impact change for perceived performance. First load hits the network; every subsequent load is instant from local cache.

Problem 5: Pagination Was Missing

The appointment list screen was loading all records at once. I replaced it with cursor-based pagination using startAfterDocument:

DocumentSnapshot? lastDoc;

Future<void> loadMore() async {
  Query query = firestore
    .collection('appointments')
    .orderBy('date', descending: true)
    .limit(15);

  if (lastDoc != null) {
    query = query.startAfterDocument(lastDoc!);
  }

  final snapshot = await query.get();
  if (snapshot.docs.isNotEmpty) {
    lastDoc = snapshot.docs.last;
    appointments.addAll(snapshot.docs.map(AppointmentModel.fromDoc));
  }
}

The Results

After applying all five fixes, here's what changed on the PDBC app:

Summary Checklist

Firestore is fast when you use it correctly. Most performance problems come from querying too broadly and listening too eagerly — both easy to fix once you know what to look for.