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

高校选修课选报平台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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 21:05:54