Next.js中getServerSideProps内存溢出问题及优化方案咨询
我想用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

