React+Firestore应用短时间内5万次读取量异常排查
我正在开发一款React应用,实现从Firestore数据库展示成员、添加新成员及删除成员的功能。但仅我一人使用时,短短几分钟内就产生了5万次读取量,而该数据库集合仅占用几KB数据。
相关代码
React组件核心逻辑
import { useState, useEffect} from "react"; import { db } from "../utils/firebase-config"; import moment from "moment/moment"; import { collection, getDocs, addDoc, deleteDoc, doc } from "firebase/firestore"; export default function MemberPage() { const membersCollectionRef = collection(db, "members"); // Create new member const [newName, setNewName] = useState(""); const [newSurname, setNewSurname] = useState(""); const [newNickname, setNewNickname] = useState(""); const [newPhone, setNewPhone] = useState(""); const [newEmail, setNewEmail] = useState(""); const [newDateOfBirth, setNewDateOfBirth] = useState(""); const [renderKey, setRenderKey] = useState(0); const createMember = async () => { await addDoc(membersCollectionRef, { name: newName, surname: newSurname, nickname: newNickname, phone: newPhone, email: newEmail, dateOfBirth: newDateOfBirth, balance: 0 }); // Clear input fields values and refresh the component setNewName(''); setNewSurname(''); setNewNickname(''); setNewPhone(''); setNewEmail(''); setNewDateOfBirth(''); setRenderKey((prevKey) => prevKey + 1); }; // Create new member // Read members from DB const [members, setMembers] = useState([]); const getMembers = async () => { const dataSnap = await getDocs(membersCollectionRef); setMembers(dataSnap.docs.map((doc) => ({ ...doc.data(), id: doc.id}))); }; useEffect(() => { getMembers(); }, [membersCollectionRef, renderKey]); // Read members from DB // Delete members from DB const deleteMember = async (id) => { const memberDoc = doc(db, "members", id); await deleteDoc(memberDoc); }; // Delete members from DB
成员列表渲染代码
{members.map((member) => { return ( <tr key={member.id}> <td>{member.name.toString()}</td> <td>{member.surname.toString()}</td> <td>{member.nickname.toString()}</td> <td>{member.phone.toString()}</td> <td>{member.email.toString()}</td> <td>{moment(Date(member.dateOfBirth.toString())).format('D/MM/YYYY')}</td> <td>{member.balance.toString()}</td> <td> <IconButton aria-label="delete" size="large" color='error' onClick={() => { deleteMember(member.id); }}> <DeleteIcon fontSize="inherit"/> </IconButton> </td> <td>update</td> </tr> ); })}
添加成员表单代码
<input type="text" value={newName} placeholder="Imię" onChange={(event) => { setNewName(event.target.value); }} /> <input type="text" value={newSurname} placeholder="Nazwisko" onChange={(event) => { setNewSurname(event.target.value); }} /> <input type="text" value={newNickname} placeholder="Ksywka" onChange={(event) => { setNewNickname(event.target.value); }} /> <input type="tel" value={newPhone} placeholder="Telefon" onChange={(event) => { setNewPhone(event.target.value); }} /> <input type="email" value={newEmail} placeholder="E-mail" onChange={(event) => { setNewEmail(event.target.value); }} /> <input type="date" value={newDateOfBirth} placeholder="Data urodzenia" onChange={(event) => { setNewDateOfBirth(event.target.value); }} /> </div> <div className="add-user"> <IconButton aria-label="add-user" size='large' color='primary' onClick={createMember}> <PersonAdd sx={{fontSize: 60}}/> </IconButton> </div>
问题原因分析
1. 无限循环的useEffect触发重复读取
collection(db, "members")每次组件渲染时都会生成一个全新的引用对象,而你的useEffect依赖项包含这个引用,导致组件每次渲染都会触发useEffect执行,进而调用getMembers发起Firestore读取。
同时,输入框的onChange会频繁更新状态(比如setNewName),状态更新触发组件重新渲染,形成循环:输入内容 → 状态更新 → 组件渲染 → 新集合引用生成 → useEffect触发 → 读取Firestore → 更新members状态 → 再次渲染... 短时间内就会产生海量读取请求。
2. 删除成员后未同步本地状态
删除成员后,你既没有更新本地members状态,也没有触发renderKey变化,导致页面显示旧数据。后续若因其他原因触发渲染,会再次发起读取请求,进一步增加读取量。
3. 全量读取的模式不够高效
当前使用getDocs每次拉取全量数据,若需实时同步,这种方式会产生不必要的重复读取。
关于Firebase监控的疑问
打开Firebase使用监控本身不会增加读取量,它只是统计已发生的请求,高读取量完全由代码逻辑的重复请求导致。
修复方案
1. 解决useEffect无限循环
用useMemo缓存集合引用,避免每次渲染生成新对象:
import { useMemo } from "react"; const membersCollectionRef = useMemo(() => collection(db, "members"), [db]);
同时简化useEffect的依赖项:
useEffect(() => { getMembers(); }, [renderKey]); // 移除membersCollectionRef
2. 删除成员后同步本地状态
在deleteMember中删除成功后,直接过滤本地数据或触发刷新:
const deleteMember = async (id) => { const memberDoc = doc(db, "members", id); await deleteDoc(memberDoc); // 方案A:直接更新本地状态 setMembers(prev => prev.filter(member => member.id !== id)); // 方案B:或触发重新读取 // setRenderKey(prev => prev + 1); };
3. 改用实时监听优化读取模式
如果需要数据实时同步,替换getDocs为onSnapshot,只需要一次订阅,后续自动同步变化:
import { onSnapshot } from "firebase/firestore"; useEffect(() => { const unsubscribe = onSnapshot(collection(db, "members"), (snapshot) => { setMembers(snapshot.docs.map(doc => ({...doc.data(), id: doc.id}))); }); // 组件卸载时取消订阅 return () => unsubscribe(); }, []);
这种方式无需手动管理renderKey,Firestore会主动推送数据变化,大幅减少读取次数。
内容的提问来源于stack exchange,提问作者Adrian Grzelka

