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

如何解决Vercel部署后出现的调用栈大小超出限制错误

Google Docs克隆项目Vercel部署后栈溢出问题解决

问题描述

开发的Google Docs克隆项目本地运行完全正常,但部署到Vercel后,登录首页出现闪屏,随后触发「maximum call stack size exceeded」(栈溢出)错误。首页核心代码如下:

"use client";

import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import Image from "next/image";
import { BsThreeDotsVertical } from "react-icons/bs";
import { Dialog, Button } from "@/lib/materialTailwind";
import axios from "axios";
import db from "@/firebase/config";
import { collection, onSnapshot, query } from "firebase/firestore";
import DocumentRow from "@/components/DocumentRow";
import Header from "@/components/Header";
import { Document } from "@/types";

export default function Home() {
  const { data: session } = useSession();

  const [showModal, setShowModal] = useState(false);
  const [newDocumentTitle, setNewDocumentTitle] = useState("");
  const [documents, setDocuments] = useState<Document[]>([]);

  const toggleModal = () => {
    setShowModal(!showModal);
  };

  const createDcoument = () => {
    axios
      .post("/api/docs", { fileName: newDocumentTitle })
      .then((res) => console.log(res.data));

    setNewDocumentTitle("");
    toggleModal();
  };

  const newDocModal = (
    <Dialog
      size="xs"
      open={showModal}
      handler={toggleModal}
      className="bg-white shadow-none h-[200px] p-5 flex flex-col justify-between"
    >
      <input
        type="text"
        value={newDocumentTitle}
        onChange={(e) => setNewDocumentTitle(e.target.value)}
        placeholder="Enter name of document..."
        onKeyDown={(e) => e.key === "Enter" && createDcoument()}
        className="outline-none w-full"
      />
      <div>
        <Button
          onClick={toggleModal}
          className="bg-transparent shadow-none hover:shadow-none text-blue-600"
        >
          CANCEL
        </Button>
        <Button color="blue" className="ml-4" onClick={createDcoument}>
          CREATE
        </Button>
      </div>
    </Dialog>
  );

  useEffect(() => {
    const q = query(collection(db, `userDocs/${session?.user?.email}/docs`));

    const unsubscribe = onSnapshot(q, (querySnapshot) => {
      let docArr: Document[] = [];

      querySnapshot.forEach((doc: any) => {
        docArr.push({ ...doc.data(), id: doc.id });
      });
      setDocuments(docArr);

      return () => unsubscribe();
    });
  }, [session?.user?.email]);

  return (
    <div>
      <Header />
      {newDocModal}
      <section className="bg-[#F0F3F4] pb-10 px-10">
        <div className="max-w-3xl mx-auto">
          <div className="py-6 flex justify-between items-center">
            <h2 className="text-gray-700 text-lg">Create a new document</h2>
            <BsThreeDotsVertical size="1.2em" />
          </div>
          <div>
            <Image
              src={
                "https://ssl.gstatic.com/docs/templates/thumbnails/docs-blank-googlecolors.png"
              }
              alt="new Google Jock"
              height={150}
              width={150}
              onClick={toggleModal}
              className="border-2 rounded-md cursor-pointer hover:border hover:border-blue-500"
            />
            <p className="mt-2 font-semibold text-sm text-gray-700">Blank</p>
          </div>
        </div>
      </section>
      <section>
        <div className="max-w-3xl mx-auto py-8">
          <table className="table-auto w-full">
            <thead>
              <tr>
                <th>Previous 30 Days</th>
                <th>Owned by anyone</th>
                <th>Last opened by me</th>
              </tr>
            </thead>
            <tbody>
              {documents.map((doc) => (
                <DocumentRow key={doc.id} doc={doc} />
              ))}
            </tbody>
          </table>
        </div>
      </section>
    </div>
  );
}

错误截图:
错误截图


问题根源

核心问题出在useEffect的Firestore监听清理逻辑上:

  • 你在onSnapshot的回调函数内部返回了清理函数,而onSnapshot本身就会返回一个用于取消监听的unsubscribe函数。这种嵌套写法会导致旧的监听无法被正确取消,每次session?.user?.email变化时,新的监听被创建,但旧监听仍在运行,触发无限的状态更新循环,最终导致栈溢出。
  • 另外,没有判断session是否存在就创建Firestore查询,可能导致空值访问,加剧渲染异常。

解决步骤

1. 修正useEffect的清理逻辑

将unsubscribe直接作为useEffect的返回值,确保每次依赖变化时,旧监听被正确取消:

useEffect(() => {
  // 先判断session是否有效,避免空值访问
  if (!session?.user?.email) return;

  const q = query(collection(db, `userDocs/${session.user.email}/docs`));
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const docArr: Document[] = [];
    querySnapshot.forEach((doc) => {
      docArr.push({ ...doc.data(), id: doc.id });
    });
    setDocuments(docArr);
  });

  // 直接返回unsubscribe作为清理函数
  return unsubscribe;
}, [session?.user?.email]);

2. 修正函数拼写错误

将createDcoument改为createDocument(注意字母o),避免后续调用时的潜在问题。

3. 可选:优化模态框渲染

可以将newDocModal改为组件内的JSX直接渲染,避免不必要的组件实例创建:

// 替换原本的newDocModal变量,直接在return里渲染
{showModal && (
  <Dialog
    size="xs"
    open={showModal}
    handler={toggleModal}
    className="bg-white shadow-none h-[200px] p-5 flex flex-col justify-between"
  >
    {/* 模态框内容不变 */}
  </Dialog>
)}

为什么仅生产环境出现问题?

  1. Next.js生产构建特性:生产环境下代码会被压缩、优化,开发环境的容错机制(如警告而非错误)被移除,原本隐藏的逻辑错误直接触发崩溃。
  2. Strict Mode差异:虽然开发环境也有Strict Mode,但本地Firestore可能使用缓存,组件渲染时序不同,没有触发无限循环;而生产环境中实时监听的触发更频繁,循环调用更快达到栈上限。
  3. Vercel执行上下文:生产环境的资源限制和执行环境与本地不同,使得栈溢出的阈值更快被触发。

内容的提问来源于stack exchange,提问作者Jevon Cochran

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 21:15:01