You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Firestore批量文档查询优化求助:NextJS页面加载过慢问题

问题背景

我用NextJS开发了一个网页,从Firestore拉取调研数据渲染图表。大部分客户每月调研量很少,但有个客户积累了数千份调研,加上当前的数据获取方式,页面加载慢到离谱,甚至触发**"Too many outstanding requests"**错误。核心瓶颈在fetchCustomersAndFeedbacks里的const customerFeedbacksSnapshot = await getDocs(customerFeedbacksQuery);这行——每次拉取单个客户的调研都要发一次请求,客户多了请求量直接爆炸。要求优化Firestore数据获取方式,同时要一次性拿到所有数据,保证前端按日期筛选时无延迟交互。

核心问题分析

当前代码采用嵌套式批量请求:先拉取所有客户,再遍历每个客户拉取他们的调研。这种模式在客户/调研量大时会产生成百上千个并发请求,既触发Firestore的请求限制,又因为请求排队导致加载超时。

优化方案

方案1:集合组查询批量拉取所有调研

利用Firestore的集合组查询,一次性拉取所有层级(业务下、门店下、服务员下)的调研数据,再在前端关联到对应的客户,彻底消除嵌套请求。

代码实现

export const getBusinessDataFromUser = async (userId: string) => {
    const userDocRef = doc(getFirebase().db, USERS_COLLECTION_NAME, userId || '');

    try {
        const [userDocSnap, businessDocSnap] = await Promise.all([
            getDoc(userDocRef),
            getBusinessDocSnap(userId)
        ]);

        const userData: User = userDocSnap.data() as User;
        const businessData: Business = businessDocSnap.data() as Business;

        const response = {
            ...businessData,
            parentId: userData.businessId,
            Id: userData.businessId,
            customers: [] as Customer[],
            feedbacks: [] as (Feedback | FeedbackHooters)[],
            sucursales: [] as Branch[],
            meseros: [] as Waiter[]
        };

        response.Icono = await getBusinessIcon(businessData);
        
        // 1. 拉取所有层级的客户、门店、服务员基础数据(不含调研)
        await Promise.all([
            fetchCustomersWithoutFeedbacks(response, businessDocRef),
            fetchBranchesWithoutFeedbacks(response, businessDocRef, userData.businessId, businessData),
            fetchMainWaitersWithoutFeedbacks(response, businessDocRef)
        ]);

        // 2. 收集所有需要关联调研的客户文档ID
        const allCustomerDocIds: string[] = [];
        response.customers.forEach(c => allCustomerDocIds.push(c.id));
        response.sucursales.forEach(branch => {
            branch.customers.forEach(c => allCustomerDocIds.push(c.id));
            branch.meseros.forEach(waiter => waiter.customers.forEach(c => allCustomerDocIds.push(c.id)));
        });
        response.meseros.forEach(waiter => waiter.customers.forEach(c => allCustomerDocIds.push(c.id)));

        // 3. 集合组查询所有feedbacks,过滤目标客户的调研
        const feedbacksQuery = collectionGroup(getFirebase().db, 'feedbacks')
            .where('__name__', 'in', allCustomerDocIds);
        const feedbacksSnapshot = await getDocs(feedbacksQuery);

        // 4. 把调研数据按客户ID分组
        const feedbackMap = new Map<string, (Feedback | FeedbackHooters)[]>();
        feedbacksSnapshot.docs.forEach(doc => {
            const customerId = doc.ref.parent.parent?.id;
            if (!customerId) return;
            const feedbackData = doc.data();
            const feedback = businessData.parentId === 'hooters' 
                ? {...feedbackData as FeedbackHooters, Visits: 0} 
                : {...feedbackData as Feedback, Visits: 0};
            
            feedbackMap.has(customerId) 
                ? feedbackMap.get(customerId)!.push(feedback) 
                : feedbackMap.set(customerId, [feedback]);
        });

        // 5. 给调研的Visits字段按顺序赋值
        feedbackMap.forEach((feedbacks) => {
            feedbacks.forEach((fb, idx) => fb.Visits = idx);
        });

        // 6. 将调研数据关联到对应层级的客户
        const assignFeedbacks = (customers: Customer[]) => {
            customers.forEach(customer => {
                customer.feedbacks = feedbackMap.get(customer.id) || [];
            });
        };
        assignFeedbacks(response.customers);
        response.sucursales.forEach(branch => {
            assignFeedbacks(branch.customers);
            branch.meseros.forEach(waiter => assignFeedbacks(waiter.customers));
        });
        response.meseros.forEach(waiter => assignFeedbacks(waiter.customers));

        return response;
    } catch (error) {
        console.error('Error al obtener información del negocio:', error);
        return null;
    }
};

// 拉取业务下的客户(不含调研)
async function fetchCustomersWithoutFeedbacks(response: any, businessDocRef: DocumentReference) {
    const customersSnapshot = await getDocs(collection(businessDocRef, 'customers'));
    response.customers = customersSnapshot.docs.map(doc => ({
        ...doc.data() as Customer,
        id: doc.id,
        feedbacks: []
    }));
}

// 拉取门店及下属客户、服务员(不含调研)
async function fetchBranchesWithoutFeedbacks(response: any, businessDocRef: DocumentReference, parentId: string, businessData: Business) {
    const branchesSnapshot = await getDocs(collection(businessDocRef, 'sucursales'));
    const branchesData = await Promise.all(branchesSnapshot.docs.map(async (branchDoc) => {
        const branch = {
            ...branchDoc.data() as Branch,
            id: branchDoc.id,
            parentId,
            customers: [] as Customer[],
            feedbacks: [] as (Feedback | FeedbackHooters)[],
            meseros: [] as Waiter[]
        };

        // 拉取门店下的客户
        const branchCustomersSnapshot = await getDocs(collection(branchDoc.ref, 'customers'));
        branch.customers = branchCustomersSnapshot.docs.map(doc => ({
            ...doc.data() as Customer,
            id: doc.id,
            feedbacks: []
        }));

        // 拉取门店下的服务员及他们的客户
        const waitersSnapshot = await getDocs(collection(branchDoc.ref, 'meseros'));
        branch.meseros = await Promise.all(waitersSnapshot.docs.map(async (waiterDoc) => {
            const waiter = {
                ...waiterDoc.data() as Waiter,
                id: waiterDoc.id,
                customers: [] as Customer[],
                feedbacks: [] as (Feedback | FeedbackHooters)[]
            };
            const waiterCustomersSnapshot = await getDocs(collection(waiterDoc.ref, 'customers'));
            waiter.customers = waiterCustomersSnapshot.docs.map(doc => ({
                ...doc.data() as Customer,
                id: doc.id,
                feedbacks: []
            }));
            return waiter;
        }));
        return branch;
    }));
    response.sucursales = branchesData;
}

// 拉取业务下的主服务员及他们的客户(不含调研)
async function fetchMainWaitersWithoutFeedbacks(response: any, businessDocRef: DocumentReference) {
    const waitersSnapshot = await getDocs(collection(businessDocRef, 'meseros'));
    const waitersData = await Promise.all(waitersSnapshot.docs.map(async (waiterDoc) => {
        const waiter = {
            ...waiterDoc.data() as Waiter,
            id: waiterDoc.id,
            customers: [] as Customer[],
            feedbacks: [] as Feedback[]
        };
        const waiterCustomersSnapshot = await getDocs(collection(waiterDoc.ref, 'customers'));
        waiter.customers = waiterCustomersSnapshot.docs.map(doc => ({
            ...doc.data() as Customer,
            id: doc.id,
            feedbacks: []
        }));
        return waiter;
    }));
    response.meseros = waitersData;
}

注意事项

  • 集合组查询需要在Firebase控制台创建对应的索引,执行代码时会收到控制台的索引创建提示,按提示操作即可。
  • 这种方式将原本成百上千的请求压缩到3-5个批量请求,大幅降低请求数和加载时间。

方案2:批量读取API(集合组查询不适用时)

如果集合组的过滤逻辑无法满足需求,可以使用Firestore的getAll API批量拉取文档引用,最多一次拉取500个文档,分批次处理也能减少请求次数。

代码片段示例

// 收集所有调研文档的引用
const allFeedbackRefs: DocumentReference[] = [];
// 遍历所有客户,收集他们的调研文档引用
// (需先拉取客户数据,再遍历每个客户的feedbacks集合获取所有文档引用)
// 分批次拉取,每500个为一批
for (let i = 0; i < allFeedbackRefs.length; i += 500) {
    const batchRefs = allFeedbackRefs.slice(i, i + 500);
    const batchSnapshots = await getAll(...batchRefs);
    // 处理这批调研数据
    batchSnapshots.forEach(snap => {
        // 映射到对应客户的逻辑
    });
}

方案3:数据预聚合(长期优化)

如果调研数据不需要实时更新,可以在后端(如Cloud Functions)定期预聚合,将每个客户的调研数据直接存储在客户文档的字段中,前端拉取客户数据时就能直接拿到调研,彻底消除嵌套请求。

Cloud Functions示例

exports.syncCustomerFeedbacks = functions.firestore
    .document('{parentPath}/customers/{customerId}/feedbacks/{feedbackId}')
    .onWrite(async (change, context) => {
        const parentParts = context.params.parentPath.split('/');
        const customerRef = doc(
            getFirebase().db,
            parentParts[0],
            parentParts[1],
            'customers',
            context.params.customerId
        );
        const feedbacksSnapshot = await getDocs(collection(customerRef, 'feedbacks'));
        const feedbacks = feedbacksSnapshot.docs.map(doc => ({
            ...doc.data(),
            id: doc.id
        }));
        // 更新客户文档的feedbacks字段
        await updateDoc(customerRef, { feedbacks });
    });

优势

  • 前端无需额外拉取调研数据,加载速度大幅提升。
  • 避免Firestore请求限制问题。

内容的提问来源于stack exchange,提问作者Daniel Erazo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 14:42:01