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:
- Average query time: dropped from ~1,400ms to ~900ms
- First-load perceived time: near-instant for returning users
- Firestore read count: down by ~60% (cheaper bill too)
- App crash rate: down because we stopped hitting Firestore's query limits
Summary Checklist
- ✅ Always filter with
.where()and.limit() - ✅ Create composite indexes for multi-condition queries
- ✅ Use
.get()for data that doesn't update frequently - ✅ Enable offline persistence in
initializeApp - ✅ Paginate large lists with
startAfterDocument
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.