Handling Firestore Query Pagination in a Scalable Way
Sunday, February 2, 2025
As your Firestore database grows, retrieving large amounts of data in a single query can become inefficient and costly. Pagination helps by breaking data retrieval into smaller, manageable chunks, improving performance and reducing Firestore read costs.
In this article, we’ll cover:
Why Pagination Matters
Firestore charges per document read. Without pagination:
By implementing pagination, you:
✅ Load data in smaller, efficient chunks
✅ Reduce unnecessary reads
✅ Improve app responsiveness
Firestore Pagination Techniques
1. Offset-Based Pagination (Inefficient)
Offset-based pagination uses .limit() and skips documents by reading all the previous ones before returning the next batch.
Example: Loading users with an offset
import { collection, query, orderBy, limit, getDocs } from "firebase/firestore";
const db = getFirestore();
const usersRef = collection(db, "users");
const q = query(usersRef, orderBy("createdAt"), limit(10));
const querySnapshot = await getDocs(q);
🔴 Problem: When you retrieve the next page, Firestore still reads all previous documents, leading to excessive reads.
2. Cursor-Based Pagination (Efficient)
A better approach is using cursors (startAfter(), startAt(), endAt(), endBefore()) to fetch only what is needed.
Step 1: Initial Query
We order data by a field (e.g., createdAt timestamp) and set a limit.
import { collection, query, orderBy, limit, getDocs } from "firebase/firestore";
const db = getFirestore();
const usersRef = collection(db, "users");
const q = query(usersRef, orderBy("createdAt"), limit(10));
const querySnapshot = await getDocs(q);
const lastVisibleDoc = querySnapshot.docs[querySnapshot.docs.length - 1];
🔹 Fetches the first 10 documents sorted by createdAt.
🔹 Stores the last document to use as a cursor.
Step 2: Fetch Next Page
Use .startAfter(lastVisibleDoc) to load the next batch efficiently.
const nextQuery = query(usersRef, orderBy("createdAt"), startAfter(lastVisibleDoc), limit(10));
const nextQuerySnapshot = await getDocs(nextQuery);
✅ Only fetches new documents, avoiding unnecessary reads.
Step 3: Fetch Previous Page (Optional)
For backward navigation, store the first document and use .endBefore(firstVisibleDoc).
const prevQuery = query(usersRef, orderBy("createdAt"), endBefore(firstVisibleDoc), limit(10));
const prevQuerySnapshot = await getDocs(prevQuery);
🔹 Fetches the previous batch efficiently.
Best Practices for Firestore Pagination
🔥 Always use indexes: Firestore requires an index for paginated queries. If an index is missing, Firestore provides a console link to create one.
🔥 Use immutable fields: createdAt timestamps work best because they never change.
🔥 Limit the number of results: Keep limits reasonable (e.g., 10–50) for smooth UX.
🔥 Store cursors in state: Save the lastVisibleDoc for seamless navigation.
Conclusion
Pagination is essential for performance and cost efficiency in Firestore. Cursor-based pagination is the most scalable approach, ensuring:
✅ Lower read costs by fetching only needed documents
✅ Faster response times compared to offset-based pagination
✅ Better UX by smoothly loading data in batches
By implementing these best practices, your Firestore app will remain fast and cost-effective. 🚀