高校选修课选报平台Firebase Firestore读取量优化求助
选修课选报平台Firestore读取量过高优化需求
我们的项目是面向本院系学生的选修课选报平台,选课时段通常为晚7点至12点。上学期仅120名学生参与时,就遭遇Firebase Firestore读取量过高的问题。我们从Spark plan切换至Blaze plan并设置1.2美元(约100卢比)的预算告警,不料选课开始仅数小时就触发告警,经排查发现访问Firebase控制台查看Firestore变更和读取量也会产生大量读取请求。
疑似读取量激增原因
- (主要)此前不知在Firebase控制台查看Firestore的变更和读取量会产生大量读取请求;
- (潜在)为已注册学生实现了在线/离线状态标识功能,用户登录时更新
online: true,登出时更新online: false。
上次120名学生6小时内消耗了25%的预算,本次预计有300-320名学生,急需优化代码以降低Firestore读取量。以下是相关组件代码,恳请提供优化建议:
ProjectSummary(选修课表格组件)
import React, { useEffect, useState } from "react"; import { useFirestore } from "../../hooks/useFirestore"; import { useLogout } from "../../hooks/useLogout"; import firebase from "firebase/app"; import "firebase/firestore"; import styles from "./ProjectSummary.module.css"; export default function ProjectSummary({ project }) { const { logout } = useLogout(); const { user } = useAuthContext(); const [isEnrolled, setIsEnrolled] = useState(false); const [isConfirming, setIsConfirming] = useState(false); const [isLoading, setIsLoading] = useState(false); const handleApprove = async () => { if (isEnrolled || isConfirming || isLoading) { return; } setIsConfirming(true); setIsLoading(true); const shouldEnroll = window.confirm("Are you sure you want to enroll?"); setIsConfirming(false); if (shouldEnroll) { try { const firestore = firebase.firestore(); const electiveRef = firestore.collection("electives").doc(project.id); const userRef = firestore.collection("users").doc(user.uid); const didEnroll = await firestore.runTransaction(async (transaction) => { const electiveDoc = await transaction.get(electiveRef); if (!electiveDoc.exists) { throw new Error("Elective document does not exist!"); } const electiveData = electiveDoc.data(); if (electiveData.slots === 0) { return false; } transaction.update(electiveRef, { slots: firebase.firestore.FieldValue.increment(-1), }); await transaction.update(userRef, { isEnroll: true, elective: project.name, subjectCode: project.details, }); return true; }); if (didEnroll) { setIsEnrolled(true); logout(); } else { alert("No available slots for enrollment."); setIsEnrolled(false); } } catch (error) { console.error("Error enrolling:", error); alert("An error occurred while enrolling. Please try again."); setIsEnrolled(false); } finally { setIsLoading(false); } } else { setIsConfirming(false); setIsLoading(false); } }; const [slots, setSlots] = useState(0); useEffect(() => { const fetchSlots = async () => { try { const electiveDoc = await firebase .firestore() .collection("electives") .doc(project.id) .get(); const electiveData = electiveDoc.data(); setSlots(electiveData.slots); } catch (error) { console.error("Error fetching slots:", error); } }; fetchSlots(); }, [project.id]); return ( <div> {isLoading && ( <div className={styles.overlay}> <div className={styles.loader}>Loading...</div> </div> )} <button className="btn" onClick={handleApprove} disabled={slots === 0 || isEnrolled || isLoading} > Enroll </button> {isEnrolled && ( <div className={styles.overlay}> <div className="confirmation-box"> <p>You have successfully enrolled</p> </div> </div> )} </div> ); }
Pending.jsx(选课页面组件)
import React, { useState, useEffect } from "react"; import { useCollection } from "../../hooks/useCollection"; import { useDocument } from "../../hooks/useDocument"; import ProjectSummary from "../project/ProjectSummary"; import { useAuthContext } from "../../hooks/useAuthContext"; export default function Pending() { const { user } = useAuthContext(); const { documents: electiveDocuments, error: electiveError } = useCollection("electives"); const { document: userDocument, error: userError } = useDocument( "users", user.uid ); const [filter, setFilter] = useState("all"); useEffect(() => { const fetchUserDocument = async () => { const { document, error } = await yourUserDocumentFetchingFunction( user.uid ); if (error) { console.error("Error fetching user document:", error); return; } setFilter(document?.department || "all"); }; fetchUserDocument(); }, [user]); const yourUserDocumentFetchingFunction = async (uid) => { const response = await fetch(`your-api-endpoint/users/${uid}`); const data = await response.json(); return { document: data, error: null }; }; const projects = electiveDocuments ? electiveDocuments.filter((document) => { switch (filter) { case "all request": return true; default: return !document.assignedUsersList.some( (assignedUser) => assignedUser?.uid.toLowerCase() == userDocument?.department.toLowerCase() ); } }) : null; return true ? ( <div className="pending-page"> <h2 className="page-title">Electives </h2> {electiveError && <p className="error">{electiveError}</p>} {userError && <p className="error">{userError}</p>} <table className="project-table"> <thead> <tr> <th>Elective Name</th> <th>Subject Code</th> <th>Click to Enroll</th> </tr> </thead> <tbody> {projects && projects.map((project) => ( <tr key={project.id}> <td className="td1">{project.name}</td> <td>{project.details}</td> <td> <ProjectSummary project={project} /> </td> </tr> ))} </tbody> </table> </div> ) : ( <div className="signup-form"> <h2>Chooser will open at 7PM</h2> </div> ); }
代码优化建议
1. 切断控制台操作产生的额外读取
选课期间禁止在Firebase控制台查看Firestore数据或监控面板,控制台的实时数据刷新会持续发起读取请求,高并发时段这部分流量会快速消耗配额。若需监控,可通过Cloud Monitoring导出数据到外部工具查看,避免直接操作控制台。
2. 消除子组件重复读取名额数据
当前ProjectSummary组件会单独请求每门课的剩余名额,若页面有10门课,每个学生打开页面就会产生10次读取请求。优化方案:
- 从父组件
Pending.jsx的electiveDocuments中直接获取slots数据,随project传递给子组件,删除ProjectSummary中的fetchSlots逻辑。
修改示例:
// ProjectSummary组件 export default function ProjectSummary({ project }) { // ... 保留原有逻辑,移除slots状态和useEffect return ( <div> {/* ... */} <button className="btn" onClick={handleApprove} disabled={project.slots === 0 || isEnrolled || isLoading} > Enroll </button> {/* ... */} </div> ); }
3. 合并用户文档的重复读取
Pending.jsx中同时使用useDocument和自定义函数获取用户数据,导致两次读取请求。保留useDocument即可,直接从返回的userDocument中提取department设置筛选条件:
// 替换原有的useEffect useEffect(() => { if (userDocument) { setFilter(userDocument?.department || "all"); } }, [userDocument]); // 删除yourUserDocumentFetchingFunction函数
4. 优化在线状态的写入逻辑
在线状态更新属于频繁小数据操作,建议:
- 若需实时心跳,降低心跳频率,或改用Firebase Realtime Database存储在线状态(按带宽收费,更适合高频小数据更新场景);
- 登出时的状态更新,可通过
beforeunload事件触发,但需注意浏览器对异步操作的限制,优先使用同步写入或配置keepAlive。
5. 缓存与批量获取数据
- 选修课基础信息(名称、代码)不会频繁变化,
useCollection可改为单次快照获取,仅对名额字段开启实时监听; - 用
localStorage缓存用户department等基础信息,避免每次打开页面都读取用户文档,仅在用户信息变更时更新缓存。
6. 事务后本地更新状态
在handleApprove的事务完成后,直接通过本地状态更新slots(若保留子组件状态),避免重新发起读取请求。
内容的提问来源于stack exchange,提问作者Gokz
相关产品推荐
相关产品推荐

