Skip to main content
Blog 6 min read

Flutter Lazy Loading: Save Firebase Costs & Boost Speed

Learn how to implement lazy loading in Flutter with Firebase to dramatically reduce database reads, save costs, and improve app performance.

F
Fabien Chung
Flutter Lazy Loading: Save Firebase Costs & Boost Speed

During the development of my latest application, LOVT, I used my trusty stack of Flutter + Firebase, a powerful and ideal combination for projects in the market testing phase, with the goal of moving quickly.

Why use Flutter and Firebase?

Flutter is a well-known framework for building cross-platform applications from a single codebase. For our project, we used Flutter Web to make the application quickly accessible.

Firebase, on the other hand, is an extremely popular backend service among developers, offering a turnkey solution with a database, removing the need for infrastructure management. This is particularly useful for us as our Flutter application is hosted and uses its database.

Firebase includes a free tier plan with quotas to follow, such as the number of reads and writes to the database, as well as the amount of files stored and downloaded.

This combo is ideal for testing a market, attracting new users, and focusing on the product, with the advantage of being free (up to a certain point).

Firebase cost savings with Flutter lazy loadingFirebase cost savings with Flutter lazy loading

The Firebase Read Quota Challenge

LOVT is an app that allows users to view service offers and requests. It features an introduction phase to present the app, followed by a list of services. The idea is to give a preview to non-registered users. Once signed up or logged in, users can access the details of each service and contact the relevant individuals.

Flutter app fetching all Firebase documents without lazy loadingFlutter app fetching all Firebase documents without lazy loading

After a soft launch to our early adopter community, I quickly realized that the initial acquisition flow was going to be problematic.

Loading the service list twice (once before logging in and again after) would soon cause us to hit the Firebase quota limit. Ten services displayed equals to ten database reads. Considering the potential growth in the number of services and registered users, we were likely to exceed the 50,000 daily read limit.

Implementing Lazy Loading in Flutter

To prevent this potential overload, I thought it was the perfect time to implement lazy loading.

Lazy loading allows data to be loaded only when needed. For large volumes of data, it boosts performance and reduces memory usage by avoiding loading everything at once. In our case, although we didn't yet have a huge amount of data, I used it to gradually load the list of services, anticipating a future increase in volume.

For the existing UI, it was sufficient to load only six results at a time, allowing users to see a full list on the first display. To view more services, they simply needed to scroll down to load six more.

Flutter app implementing lazy loading pagination with FirebaseFlutter app implementing lazy loading pagination with Firebase

From a coding perspective, it looks like this:

In my repository class, I build the Firestore query by limiting the number of results to 6.

dart34 lines
1Future<PaginatedJobPosts> getJobsAvailable( 2 {DocumentSnapshot? lastDocument, int limit = 6}) async { 3 4 try { 5 6// Define a query with a limit 7 Query query = _firestore.collection("jobPosts").limit(limit); 8 9 if (lastDocument != null) { 10 query = query.startAfterDocument(lastDocument); // continue after the last document 11 } 12 13// Map the data from firestore 14 final querySnapshot = await query 15 .withConverter<JobPost>( 16 fromFirestore: (snapshot, _) => JobPost.fromMap(snapshot.data()!), 17 toFirestore: (job, _) => job.toMap(), 18 ) 19 .get(); 20 21// Prepare the data to return 22 final jobs = querySnapshot.docs.map((doc) => doc.data()).toList(); 23 24 final lastDoc = 25 querySnapshot.docs.isNotEmpty ? querySnapshot.docs.last : null; 26 27// Return an object PaginatedJobPosts to be manipulated in the view 28 return PaginatedJobPosts(jobs, lastDoc); 29 30 } catch (e) { 31 debugPrint("Error fetching jobs: $e"); 32 return PaginatedJobPosts([], null); 33 } 34 }

Then, in the view, I display the query results and handle loading additional data.

dart35 lines
1// NotificationListener to handle scroll down event 2return NotificationListener<ScrollNotification>( 3 onNotification: (ScrollNotification scrollInfo) { 4 5 // Check if it is the bottom of the list 6 if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && 7 !isLoadingMore) { 8 _loadMoreJobs(); // Request more results from firebase 9 } 10 return false; 11 }, 12 13 // Load the list view 14 child: ListView.builder( 15 itemCount: _allJobs.length + (_hasMoreData ? 1 : 0), 16 itemBuilder: (context, index) { 17 18 if (index == _allJobs.length) { 19 return const Center(child: CircularProgressIndicator()); 20 } 21 22 return Container( 23 margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), 24 child: JobPostCard( 25 jobPost: _allJobs[index], 26 onTap: () => context.goNamed( 27 AppRoute.jobDetail.name, 28 pathParameters: {'id': index.toString()}, 29 extra: _allJobs[index], 30 ), 31 ), 32 ); 33 }, 34 ), 35 );
dart19 lines
1Future<void> _loadMoreJobs() async { 2 // Manage loader 3 if (ref.read(loadingNotifierProvider) || !_hasMoreData) return; 4 ref.read(loadingNotifierProvider.notifier).setLoading(true); 5 6 // Call repository to get more results 7 final paginatedJobs = 8 await ref.read(getJobsAvailableProvider(_lastDocument).future); 9 10 if (paginatedJobs.jobs.isEmpty) { 11 _hasMoreData = false; 12 } else { 13 _allJobs.addAll(paginatedJobs.jobs); 14 _lastDocument = paginatedJobs.lastDocument; 15 } 16 17 // Manage loader 18 ref.read(loadingNotifierProvider.notifier).setLoading(false); 19 }

Code details here

By applying this loading limit, the number of database reads significantly decreased. Firebase only counts the elements returned in the query, meaning that if you load six results, it counts as six reads — far fewer than if you were to load all the data at once.

Firebase Read Quota Reduction Results

Did the implementation of lazy loading actually help?

Thanks to a highly effective TikTok campaign, we experienced a significant spike in acquisitions, allowing us to test and confirm the effectiveness of this optimization.

So, how did the performance compare before and after lazy loading?

Before optimization: With around 200 sign-ups, we were close to 43,000 readings in the database.

High Firebase database reads before Flutter lazy loading optimizationHigh Firebase database reads before Flutter lazy loading optimization

After optimization: With nearly 650 sign-ups, the peak number of reads dropped to around 37,000. This means we tripled the number of sign-ups while reducing the read quota!

Reduced Firebase database reads after Flutter lazy loading optimizationReduced Firebase database reads after Flutter lazy loading optimization

Benefits of Lazy Loading in Flutter

- Cost reduction: We stayed within Firebase's free tier.

- Performance improvement: Faster load times for users.

- Memory savings: By avoiding loading the entire list of services at once.

- Scalability: The app can handle more users without increasing costs.

In summary, lazy loading has proven to be very effective for our app. Not only did it allow us to stay within Firebase's free tier, but it also gave us better scalability margins.

For a startup like LOVT, with limited resources, every way to optimize and save matters. The trio of Flutter, Firebase, and lazy loading is validated on my end. What do you think?

Successful Flutter and Firebase lazy loading implementationSuccessful Flutter and Firebase lazy loading implementation