نشتی حافظه ۱.۸ گیگابایتی در Next.js: یک خط کانفیگ، ۴.۴ گیگ مصرف رم
نویسنده: آرش لطیفی
باگ واقعی تیم Logos: دایرکتیو Edge runtime روی مسیر OG image باعث شد `@resvg/resvg-wasm` هر رندر حافظه لوک کن و کانتینر بعد از OOM ریاستارت شود. علت، جدول اندازهگیری و راهحل.
TL;DR — یک خط
export const config = { runtime: 'edge' }روی مسیر OG image باعث شد اپ Next.js تیم Logos در ۳ دقیقه به ۴.۴ گیگ رم برسد؛ علت نشتی شناختهشدهی@resvg/resvg-wasmبود و حذف همون یک خط، مصرف را حدود ۱.۸ گیگ پایین آورد.
این یکی از اون باگهاییه که میترسونه چون بیصدا است. تیم پلتفرم نشر Logos (نوشته سیدارث کومار، مارس ۲۰۲۶) موقع لود تست دید کانتینرشون ظرف ۳ دقیقه به ۴.۴ گیگ حافظه میرسه، بعد OOM میخوره و ریاستارت میشه — و دوباره همون چرخه. لاگها RuntimeError: unreachable از یه ماژول WASM داخل og.js نشون میداد.
ریشه مشکل: Edge runtime روی Node.js
بالای مسیر تولید تصویر OG این خط بود:
export const config = { runtime: 'edge' }
روی Vercel Edge یا Cloudflare Workers مشکلی نیست. اما وقتی روی یه محیط Node.js استاندارد دیپلوی میکنی، Next.js همچنان سعی میکنه این دایرکتیو رو اجرا کنه. چون رندرر داخلی Edge Runtime در دسترس نیست، به @resvg/resvg-wasm برمیگرده — همون چیزی که @vercel/og زیر هود برای رندر SVG استفاده میکنه. و resvg-wasm یه نشتی حافظه شناختهشده داره: هر رندر حافظه WASM رزرو میکنه که هرگز آزاد نمیشه. روی مسیری که برای هر صفحه سایت OG میسازه، این خیلی سریع جمع میشه.
راهحل؟ حذف همون یک خط. مسیر به هندلر استاندارد Node.js برمیگرده و بهجای مسیر WASM لوککننده، از next/og استفاده میکنه.
عدد واقعی: قبل و بعد
نویسنده همون لود تست رو روی برنچ master و برنچ فیکس اجرا کرد و هر ۳۰ ثانیه RSS و heap رو نمونهبرداری کرد:
| زمان | RSS (master) | Heap (master) | RSS (فیکس) | Heap (فیکس) | | --- | --- | --- | --- | --- | | 18:08:21 | 1194 MB | 57 MB | 1424 MB | 46 MB | | 18:09:22 | 2690 MB | 311 MB | 2514 MB | 152 MB | | 18:10:23 | 3720 MB | 994 MB | 2553 MB | 109 MB | | 18:11:24 | 4404 MB | 1580 MB | 2571 MB | 148 MB |
بعد از ۳ دقیقه، master روی 4404 مگابایت RSS بود و heap هنوز بالا میرفت. برنچ فیکس حدود 2571 مگابایت تثبیت شد — یعنی حدود ۱۸۳۳ مگابایت صرفهجویی، و مهمتر از عدد، heap تخت بود نه صعودی.
دو نشتی دیگه که سر راه پیدا شد
- کش تصویر Placeholder: سرویس placeholder استرینگهای base64 رو توی یه
Mapبدون eviction نگه میداشت؛ هر تصویر توی ISR یه entry اضافه میکرد که هیچوقت حذف نمیشد. با سقف FIFO روی ۵۰۰ ورودی حل شد. - کش ایندکس جستجو:
PostSearchServiceایندکسهای معکوس Lunr رو بهازای پارامترهای کوئری انبار میکرد — باز هم رشد بینهایت. سقف ۵۰ ورودی با eviction FIFO گذاشت.
هیچکدوم نشتی اصلی نبودن، ولی روی سرور لانگرانینگ دیر یا زود دردسر میشدن. یه الگوی دیگه هم فیکس شد: فونت با fetch روی هر درخواست OG لود میشد که با fs.readFileSync در سطح ماژول و کش بافر جایگزین شد:
چکلیست: اپ Next.js تو مستعد این باگه؟
- [ ] بگرد دنبال
runtime: 'edge'توی پروژه؛ اگه روی Node.js استاندارد دیپلوی میکنی، لازمش نداری - [ ] مسیر OG image (
opengraph-image.tsxیا مسیرog) رو پیدا کن و ببین کدوم رندرر استفاده میشه - [ ] اگه
@vercel/ogیاresvg-wasmمیبینی، حافظه کانتینر رو زیر لود تست مانیتور کن - [ ] همه
Mapها و کشهای سرور رو چک کن که سقف/eviction دارن - [ ] فونتها و assetهای سنگین رو یکبار در سطح ماژول لود کن، نه توی هندلر درخواست
- [ ] یه alert برای RSS بالای آستانه بذار تا نشتی قبل از OOM دیده بشه
درس بزرگتر
این باگ یادآوری میکنه که دایرکتیوها رو کپی-پیست نکن بدون اینکه بدونی کجا معنا دارن. runtime: 'edge' یه پیشنهاد نیست؛ یه قرارداده که در محیط اشتباه، مسیر اجرای کاملاً متفاوتی فعال میکنه. اگه اپت CPU یا رم رو بدون دلیل جویده، اول از مسیرهای رندرینگ تصویر و WASM شروع کن، بعد سراغ کشهای بدون سقف برو. برای بهینهسازی خود تصاویر هم راهنمای بهینهسازی تصویر در Next.js رو ببین، و برای ابزارهای بهروز ماندن، آموزش ابزارهای Agent در Next.js 16.4 مکمل خوبیه.
Q: من روی Vercel دیپلوی میکنم، این باگ من رو تهدید میکنه؟
نه به این شکل. مشکل وقتیه که دایرکتیو Edge روی یه محیط Node.js استاندارد (سرور خودت، Docker، VPS) باشه. روی Vercel، مسیرهای Edge واقعاً روی Edge اجرا میشن و این fallback اتفاق نمیافته — ولی اگه بعداً سلفهاست کنی، همون خط میشه بمب ساعتی.
Q: چطور بفهمم اپ من داره حافظه لوک میکنه؟
سه نشونه: RSS کانتینر بهمرور فقط بالا میره و پایین نمیاد، heap توی پروفایلر خطی رشد میکنه، و کانتینر بهطور دورهای با OOMKilled ریاستارت میشه. سادهترین کار: RSS رو هر ۳۰ ثانیه لاگ کن و نمودارش رو ببین — دقیقاً همون کاری که این تیم کرد.
Q: next/og با @vercel/og چه فرقی داره؟
طبق نوشته اصلی، وقتی مسیر روی هندلر استاندارد Node.js اجرا میشه، از next/og استفاده میشه و از مسیر WASM لوکدار @vercel/og/resvg-wasm عبور میکنه. یعنی همون قابلیت تولید OG image، بدون نشتی — با این شرط که دایرکتیو Edge رو برداری.