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

Next.js 15项目出现JavaScript堆内存溢出致命错误求助

Next.js 15 内存溢出问题排查

问题现象

Next.js 15项目频繁触发FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory错误,每日重启服务后仍会复现。网站大部分内容通过SSR加载,基于会话进行身份验证。

错误日志

2025-09-21T18:50:49: [905257:0x25f7a690] 48491672 ms: Scavenge 1966.7 (2083.4) -> 1960.5 (2083.4) MB, 9.7 / 0.1 ms (average mu = 0.948, current mu = 0.947) allocation failure;
2025-09-21T18:50:49: [905257:0x25f7a690] 48493391 ms: Scavenge 1965.9 (2083.4) -> 1960.9 (2083.4) MB, 8.5 / 0.0 ms (average mu = 0.948, current mu = 0.947) task;
2025-09-21T18:50:49: [905257:0x25f7a690] 48493878 ms: Scavenge 1967.2 (2083.4) -> 1962.4 (2099.4) MB, 16.0 / 0.1 ms (average mu = 0.948, current mu = 0.947) task;

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

核心代码片段

app/middleware.ts

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;
  const url = req.nextUrl.clone();

  if (pathname.match(/^\/brand\/[^\/]+\/[^\/]+\/[^\/]+\/?$/)) {
    const parts = pathname.split('/');
    const refnum = parts[4];

    if (/\%20|[. ~A-Z]/.test(refnum)) {
      const cleanRefnum = refnum
        .replace(/%20/g, '-')
        .replace(/[. ~]/g, '-')
        .toLowerCase()
        .replace(/-+/g, '-')
        .replace(/^-|-$/g, '');

      parts[4] = cleanRefnum;
      url.pathname = parts.join('/');
      return NextResponse.redirect(url, 301);
    }
  }

  if ( pathname.startsWith('/new-products/') ) {
    return NextResponse.redirect(new URL('/products/', req.url))
  }

  const response = NextResponse.next();
  try{
    const res = await fetch(process.env.NEXT_PUBLIC_API_URL + "api/customer/auth/info", {
        cache:"no-cache",
        headers: {
        cookie: req.headers.get("cookie") || "",
        },
        credentials:"include"
    });
    const sessionData = await res.json();
    response.headers.set("x-session", JSON.stringify(sessionData.session || {}));
  }catch(err){
    console.error("Middleware Error : "+err.message)   
    response.headers.set("x-session", JSON.stringify({}));    
  }
  return response;
  
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon\\.ico|.*\\.(?:ico|png|jpg|jpeg|webmanifest)$).*)'
  ],
};

首页page.ts

import { Metadata } from "next"
import { Suspense } from "react"
import { headers } from "next/headers"

import { PageDataType } from "@/types"
import axiosInstance from "@/lib/axiosInstance"
import PageContent from "./(home)/_components/page-content"

export const metadata: Metadata = {
    title: "Random Website",
    description: "This is a test description",
    alternates: {
        canonical: `${process.env.BASEURL}`
    }
}

export default async function Home() {
    const sessionData = JSON.parse(await (await headers()).get("x-session") || "{}")
    let pageData: PageDataType = {}

    try {
        const [homeResponse, faqResponse, blogResponse, testinomialResponse] = await Promise.all([
            axiosInstance.post("/site/data", {}).catch((error) => ({ error, data: null })),
            axiosInstance.post("/faq/list", { }).catch((error) => ({ error, data: null })),
            axiosInstance.get("https://blog.mysite.sg/wp-json/wp/v2/posts?page=1&per_page=6", {}).catch((error) => ({ error, data: null })),
            axiosInstance.post("/ratings/list", {"username": "website"})
        ])
        pageData = {
            ...homeResponse.data,
            ...faqResponse.data,
            blogData: blogResponse.data,
            testinomialData: testinomialResponse.data.ratings
        }
    } catch (error) {
        console.log("Error fetching data:", error)
    }

    return (
        <Suspense>
            <PageContent
                pageData={pageData}
                sessionData={sessionData}
            />
        </Suspense>
    )
}

排查分析

  1. Middleware 全局请求累积内存占用

    • Middleware的matcher覆盖了几乎所有非静态资源请求,每次请求都会发起/api/customer/auth/info的无缓存fetch调用,高并发场景下会导致大量请求实例堆积,内存无法及时回收。
    • 每次将session数据序列化为JSON存入响应头,若session数据体积较大,频繁的序列化操作会进一步增加内存开销。
  2. SSR数据加载的内存泄漏风险

    • 首页同时发起4个API请求,若axiosInstance未配置请求取消机制,当请求超时或页面被中断时,未完成的请求实例会占用内存无法释放。
    • 若接口返回的数据量过大(如博客、评价数据),SSR阶段每个请求都会加载完整数据,长期累积会导致堆内存耗尽。
  3. Node.js 默认堆内存限制不足

    • 从日志看堆内存已接近2GB,超过Node.js默认的堆内存限制(约1.4GB),这会直接触发内存溢出错误。

修复建议

  • 优化Middleware逻辑

    • 为/api/customer/auth/info请求添加缓存策略,比如设置cache: "force-cache"并配置revalidate,减少重复请求;或通过NextResponse.cache()设置缓存头,避免每次请求都重新获取session。
    • 缩小matcher范围,仅匹配需要会话验证的路由,避免全局触发Middleware逻辑。
  • 优化SSR数据获取

    • 检查接口返回数据量,对大体积数据做分页处理,只加载当前页面所需内容。
    • 为axios请求添加AbortController,在组件卸载或请求超时后取消未完成的请求,释放内存。
    • 将非关键数据(如博客、评价)改为客户端组件加载,降低SSR阶段的内存占用。
  • 调整Node.js堆内存限制

    • 在启动命令中添加--max-old-space-size参数,例如next start --max-old-space-size=4096,将堆内存限制提升至4GB,临时缓解内存不足问题(需配合代码优化才能彻底解决)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 07:44:55