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

Next.js中getServerSideProps内存溢出问题及优化方案咨询

问题:Next.js读取单个JSON文件内存占用过高及大量JSON数据生成动态页面方案

我想用getServerSideProps渲染动态页面,未来数据会从数据库获取,开发阶段暂时存在本地JSON文件中。
说明:总数据约13GB,存储在16000个JSON文件里,每个文件对应一个动态页面(最大文件约7MB)。
为什么Next.js读取单个文件时内存占用能超过4GB?针对大量JSON数据生成动态页面,有什么推荐方案?


项目结构

数据存储在 /data/stocks/
从 /pages/stocks/[ticker].js 访问

数据获取代码

export const getServerSideProps = async (context) => {
  const { ticker } = context.params;

  // 从json读取tickers
  const tickers = require('../../data/tickers.json');
  const id = tickers[ticker];
  const zeroPad = (num, places) => String(num).padStart(places, '0')

  const cik = 'CIK' + zeroPad(id, 10)

  // 从json获取数据
  const data = require('../../data/' + cik + '.json');  
  return {
    props: {
      cik: cik,
    }
  }
}

报错输出

<--- Last few GCs --->

[27496:0000014E055DB500]  3518357 ms: Mark-sweep (reduce) 2038.9 (2075.1) -> 2038.7 (2073.3) MB, 288.3 / 0.0 ms  (+ 49.7 ms in 13 steps since start of marking, biggest step 7.0 ms, walltime since start of marking 353 ms) (average mu = 0.434, current mu = 

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
 1: 00007FF64BA07A1F v8::internal::CodeObjectRegistry::~CodeObjectRegistry+114207
 2: 00007FF64B996096 DSA_meth_get_flags+65542
 3: 00007FF64B996F4D node::OnFatalError+301
 4: 00007FF64C2CB2CE v8::Isolate::ReportExternalAllocationLimitReached+94
 5: 00007FF64C2B58AD v8::SharedArrayBuffer::Externalize+781
 6: 00007FF64C158C7C v8::internal::Heap::EphemeronKeyWriteBarrierFromCode+1468
 7: 00007FF64C155D94 v8::internal::Heap::CollectGarbage+4244
 8: 00007FF64C153710 v8::internal::Heap::AllocateExternalBackingStore+2000
 9: 00007FF64C171420 v8::internal::FreeListManyCached::Reset+1408
10: 00007FF64C171AD5 v8::internal::Factory::AllocateRaw+37
11: 00007FF64C1871AB v8::internal::FactoryBase<v8::internal::Factory>::NewRawOneByteString+75
12: 00007FF64C17FABC v8::internal::Factory::NewStringFromUtf8+124
13: 00007FF64C2C7C0A v8::String::NewFromUtf8+202
14: 00007FF64B8AA9E1 v8::internal::OSROptimizedCodeCache::OSROptimizedCodeCache+33841
15: 00007FF64B9B16E7 v8::internal::Malloced::operator delete+3447
16: 00007FF64C285CA6 v8::internal::Builtins::code_handle+172790
17: 00007FF64C285899 v8::internal::Builtins::code_handle+171753
18: 00007FF64C285B5C v8::internal::Builtins::code_handle+172460
19: 00007FF64C2859C0 v8::internal::Builtins::code_handle+172048
20: 00007FF64C3590C1 v8::internal::SetupIsolateDelegate::SetupHeap+494673
21: 0000014E0735B74B

解答

为什么读取单个JSON会内存占用过高?

核心原因是你用了require()加载JSON文件。Node.js的require()会缓存模块,一旦加载过的JSON文件会一直留在内存中,不会被垃圾回收机制回收。当用户访问多个不同页面后,越来越多的JSON文件被缓存,最终导致内存溢出。哪怕你只用到了cik字段,require()还是会把整个JSON文件解析成JS对象并缓存,这对大文件来说内存消耗极大。

另外,开发模式下Next.js开启了热重载,缓存机制的影响会被放大,内存不会在请求结束后释放,进一步加剧内存占用问题。

推荐方案

1. 替换require()为文件流读取+按需解析

不用require()加载整个JSON,而是用Node.js的fs模块读取文件内容,并且只提取需要的字段(比如你这里其实已经生成了cik,根本不需要加载目标JSON文件)。如果确实需要文件里的其他字段,可以用JSON流解析库(比如JSONStream)避免一次性加载整个文件到内存:

import fs from 'fs/promises';
import { parse } from 'JSONStream';
import { pipeline } from 'stream/promises';

export const getServerSideProps = async (context) => {
  const { ticker } = context.params;

  // 读取tickers.json,用fs.readFile替代require
  const tickersBuffer = await fs.readFile('../../data/tickers.json');
  const tickers = JSON.parse(tickersBuffer.toString());
  const id = tickers[ticker];
  const zeroPad = (num, places) => String(num).padStart(places, '0');
  const cik = 'CIK' + zeroPad(id, 10);

  // 若需要目标JSON里的特定字段,用流解析只提取该字段
  let targetField = null;
  await pipeline(
    fs.createReadStream(`../../data/${cik}.json`),
    parse('$.targetField'), // 指定要提取的字段路径
    async (source) => {
      for await (const chunk of source) {
        targetField = chunk;
      }
    }
  );

  return {
    props: {
      cik: cik,
      targetField
    }
  };
};

2. 开发阶段临时提升Node.js内存限制

如果暂时不想改代码,可以在启动命令里增加内存限制,缓解溢出问题:

NODE_OPTIONS="--max-old-space-size=8192" next dev

这会把V8的堆内存上限调到8GB,但这只是临时方案,生产环境不推荐依赖此方法。

3. 提前预生成静态页面(SSG)

既然每个JSON对应一个静态页面,开发阶段可以用Next.js的getStaticPaths + getStaticProps预生成页面,避免请求时动态读取文件,大幅降低内存占用。16000个页面预生成耗时较长,开发阶段可以只生成部分路径:

import fs from 'fs/promises';

export async function getStaticPaths() {
  // 开发阶段只生成前100个路径,生产环境再生成全部
  const tickersBuffer = await fs.readFile('../../data/tickers.json');
  const tickers = JSON.parse(tickersBuffer.toString());
  const paths = Object.keys(tickers).slice(0, 100).map(ticker => ({
    params: { ticker }
  }));

  return { paths, fallback: 'blocking' }; // fallback: blocking会按需生成未预生成的页面
}

export async function getStaticProps(context) {
  const { ticker } = context.params;
  const tickersBuffer = await fs.readFile('../../data/tickers.json');
  const tickers = JSON.parse(tickersBuffer.toString());
  const id = tickers[ticker];
  const zeroPad = (num, places) => String(num).padStart(places, '0');
  const cik = 'CIK' + zeroPad(id, 10);

  const dataBuffer = await fs.readFile(`../../data/${cik}.json`);
  const data = JSON.parse(dataBuffer.toString());

  return {
    props: {
      cik: cik,
      // 传入需要的字段
    },
    revalidate: 86400 // 可选,设置页面自动重新生成的时间(秒)
  };
}

4. 过渡到轻量数据库(开发阶段可用)

既然未来要用到数据库,开发阶段可以先用SQLite这类轻量数据库,把JSON数据批量导入进去,查询时只获取需要的字段,避免加载整个JSON文件:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const getServerSideProps = async (context) => {
  const { ticker } = context.params;
  const stock = await prisma.stock.findUnique({
    where: { ticker },
    select: { cik: true } // 只查询需要的字段
  });

  return {
    props: {
      cik: stock?.cik
    }
  };
};

总结

优先解决require()缓存的问题,用fs.readFile或流解析替代;开发阶段可以临时提升内存或采用SSG方案;长期方案是尽快迁移到数据库,从根源解决大量JSON文件带来的内存和性能问题。


内容的提问来源于stack exchange,提问作者Ph.lpp

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 01:54:24