Server ngốn gần 10 GB RAM vì dữ liệu dùng chung bị lấy lại ở mỗi request
Một server dev chiếm 9,8 GB sau 2.000 request, bản chạy thật chỉ 150 MB. Gốc là mẫu lỗi chung của mọi loại server: dữ liệu dùng chung bị đọc lại ở mỗi request. Số đo từ một dự án Next.js, cách sửa và cách rà app của bạn, dùng framework nào cũng áp dụng được.
Sau hơn bốn tiếng chạy next dev qua nhiều vòng test E2E, Activity Monitor báo tiến trình next-server của một web app chiếm 16,75 GB RAM, trong đó 14 GB đã bị hệ điều hành nén. App đang thêm tiếng Anh bên cạnh tiếng Việt, bản dịch nằm trong thư mục messages/ gồm khoảng 70 file JSON nhỏ cho mỗi ngôn ngữ.
Không có dòng lỗi nào, trang vẫn trả đúng. RAM chỉ tăng. Đo lại cho gọn: khởi động server sạch, bắn 2.000 request vào 10 trang, rồi đo.
Chế độ
Bộ nhớ sau 2.000 request
next dev, trước khi sửa
9,8 GB (ép GC xong vẫn còn 7,1 GB heap sống)
next start, bản build production
128 đến 167 MB
Bản chạy thật không bị. Vậy lỗi nằm ở chế độ dev, và khi điều tra thì hóa ra có hai nguyên nhân khác nhau.
Mẫu lỗi này không riêng Next.js
Chi tiết dưới đây đo trên Next.js, nhưng cái bẫy thì có ở mọi loại server: Express, Django, Rails, Laravel, Spring, Go. Chỗ nào code chạy ở mỗi request mà lại lấy một thứ mà request nào cũng cần như nhau (bản dịch, cấu hình, danh mục, template email, prompt, kết quả gọi API ngoài), thì bạn trả cùng một chi phí hàng nghìn lần. Thường nó chỉ hiện ra là CPU và độ trễ. Nó thành RAM khi có thứ gì đó giữ lại kết quả của mỗi lần lấy: dấu vết gỡ lỗi của chế độ dev, một Map không bao giờ xóa, bộ nhớ đệm của ORM gắn theo từng request, log kèm cả payload.
Mình chỉ đo trên Next.js nên không nói được con số cho framework khác. Cách giữ lại khác nhau ở mỗi nơi, còn gốc thì giống nhau.
Nguyên nhân thứ nhất: đọc lại 70 file ở mỗi request
Hàm nạp bản dịch trông như thế này, kiểu code AI viết rất tự nhiên:
const cache = new Map()async function load(locale: string) { const hit = cache.get(locale) if (hit && process.env.NODE_ENV === 'production') return hit // chỉ dùng cache khi chạy thật const out = await readAllFiles(locale) // đọc ~70 file cache.set(locale, out) return out}
Chữ production có lý do: lúc phát triển bạn sửa một câu chữ và muốn thấy ngay, nên cache bị tắt. Hậu quả là ở dev, mọi request đều đọc lại 70 file. Đo bằng sampling heap profiler thì các mục bị giữ nhiều nhất là hàm đọc bản dịch và hàm chụp stack trace mà runtime dev của Next/React gọi cho từng promise, kể cả promise của readFile. Nói gọn: bản dev ghi lại dấu vết mọi lần đọc, và 70 lần mỗi request thì dấu vết chất đống.
Cách sửa gồm hai thay đổi nhỏ. Cache chính promise, không đợi có kết quả mới cache, để mười request đồng thời cùng chờ một lần đọc:
const cache = new Map<string, Promise<Messages>>()function load(locale: string) { let hit = cache.get(locale) if (!hit) { hit = readAllFiles(locale) hit.catch(() => cache.delete(locale)) // lỗi thì lần sau thử lại cache.set(locale, hit) } return hit}
Và giữ tiện lợi của dev bằng cách xóa cache khi file đổi, thay cho việc tắt hẳn cache:
Đã kiểm tra: sửa một file bản dịch thì trang đổi ngay, trả lại thì về như cũ.
Nguyên nhân thứ hai: lỗi riêng của Next.js dev
Sau bản sửa, server sạch bắn riêng trang /lien-he vẫn thấy heap sau GC tăng theo đường thẳng: 290, 628, 967, 1.303 MB, mỗi mốc 500 request. Một trang admin trống chỉ có root layout cũng tăng khoảng 1,2 MB mỗi request.
Người điều tra dựng một app Next trống, không có dòng code nào của dự án. Root layout gọi await headers(), trang chỉ có một thẻ h1:
// app/layout.jsximport { headers } from 'next/headers'export default async function L({ children }) { await headers() return <html><body>{children}</body></html>}// app/page.jsxexport default function P() { return <h1>hi</h1> }
Chạy next dev, gọi / 1.500 lần, đo heap sau khi ép GC:
Phiên bản Next
Heap sau 1.500 request
16.4.2
90 → 343–350 MB
16.4.3
82 → 334 MB
16.5.0-canary.27
80 → 335 MB
Đối chứng trên cùng app, mỗi lần 1.500 request:
Thay đổi
Kết quả
await connection() trong layout thay cho headers()
91 → 109 MB
Không gọi API động nào
88 → 126 MB
headers() đặt trong server component con bọc children
vẫn rò (90 → 348 MB)
Mức rò còn tăng theo kích thước dữ liệu mà trang gửi xuống trình duyệt. App này đang đưa toàn bộ khoảng 240 KB bản dịch xuống mọi trang qua NextIntlClientProvider. Bỏ phần đó thì mức rò từ 1,2 xuống khoảng 0,43 MB mỗi request. Ở app trống, payload lớn nhưng không có headers() thì không rò.
Mình chưa thấy issue tương ứng ở kho Next.js, và người điều tra chưa gửi. Nếu bạn gặp đúng tổ hợp này, đây là kịch bản tái hiện nhỏ nhất.
Ý nghĩa
Hai nguyên nhân cộng lại. Lỗi của Next làm mỗi request để lại một ít. Đoạn code đọc 70 file làm cái “một ít” đó to ra nhiều lần. Sửa cache bản dịch kéo con số từ 9,8 GB xuống mức chấp nhận được cho một buổi làm việc, nhưng không đưa nó về phẳng như production.
Hai bài học nhỏ:
Trước khi đổ lỗi cho code của mình, hoặc cho framework, hãy dựng một app trống làm đối chứng. Không có đối chứng thì chỉ có suy đoán.
Dữ liệu dùng chung, ít đổi, đừng đọc lại ở mỗi request, dù bạn dùng server nào. Bản dịch, cấu hình, danh mục, template email, prompt của chatbot đều thuộc nhóm này. Cache promise, xóa cache đúng lúc dữ liệu đổi.
Kiểm tra app của bạn
Chạy dev, ghi RAM tiến trình server, rồi gọi một trang 500 lần: for n in $(seq 1 500); do curl -s -o /dev/null localhost:3000/trang-cua-ban; done. Đo lại bằng footprint -f bytes <pid> (macOS) hoặc top. Tăng đều và không hạ là có vấn đề.
Chạy cùng kịch bản trên bản build chạy thật của app. Nếu bản đó đứng yên thì phần lớn là chuyện của chế độ dev, đừng vội sửa code chạy thật. Nếu bản thật cũng tăng thì lỗi nằm ở code của bạn.
Tìm các điều kiện kiểu NODE_ENV === 'production' hay DEBUG đứng cạnh một cache, và các readFile, fetch, truy vấn database nằm trong code chạy ở mỗi request mà kết quả không phụ thuộc người gọi.
Riêng với Next.js: nếu layout gốc của bạn gọi await headers() hay cookies(), thử đổi sang await connection() hoặc chuyển phần cần header xuống sâu hơn để xem bộ nhớ có hết tăng không. Chỉ đo, chưa chắc áp dụng được cho app của bạn.
Đừng đưa toàn bộ bản dịch xuống trình duyệt khi trang chỉ dùng vài nhóm chữ.
hiệu năngbộ nhớvibe code/anh/web/du-lieu-dung-chung-doc-lai-moi-request-an-het-ram-480.webphiệu năng · bộ nhớ · vibe code
Server ngon gan 10 GB RAM vi du lieu dung chung bi lay lai o moi request Mot server dev chiem 9,8 GB sau 2.000 request, ban chay that chi 150 MB. Goc la mau loi chung cua moi loai server: du lieu dung chung bi doc lai o moi request. So do tu mot du an Next.js, cach sua va cach ra app cua ban, dung framework nao cung ap dung duoc. hieu nang bo nho vibe code Sau hon bon tieng chay `next dev` qua nhieu vong test E2E, Activity Monitor bao tien trinh `next-server` cua mot web app chiem 16,75 GB RAM, trong do 14 GB da bi he dieu hanh nen. App dang them tieng Anh ben canh tieng Viet, ban dich nam trong thu muc `messages/` gom khoang 70 file JSON nho cho moi ngon ngu.

Khong co dong loi nao, trang van tra dung. RAM chi tang. Do lai cho gon: khoi dong server sach, ban 2.000 request vao 10 trang, roi do.
| Che do | Bo nho sau 2.000 request |
|---|---|
| `next dev`, truoc khi sua | 9,8 GB (ep GC xong van con 7,1 GB heap song) |
| `next start`, ban build production | 128 den 167 MB |
Ban chay that khong bi. Vay loi nam o che do dev, va khi dieu tra thi hoa ra co hai nguyen nhan khac nhau.
## Mau loi nay khong rieng Next.js
Chi tiet duoi day do tren Next.js, nhung cai bay thi co o moi loai server: Express, Django, Rails, Laravel, Spring, Go. Cho nao code chay o moi request ma lai lay mot thu ma request nao cung can nhu nhau (ban dich, cau hinh, danh muc, template email, prompt, ket qua goi API ngoai), thi ban tra cung mot chi phi hang nghin lan. Thuong no chi hien ra la CPU va do tre. No thanh RAM khi co thu gi do giu lai ket qua cua moi lan lay: dau vet go loi cua che do dev, mot Map khong bao gio xoa, bo nho dem cua ORM gan theo tung request, log kem ca payload.
Minh chi do tren Next.js nen khong noi duoc con so cho framework khac. Cach giu lai khac nhau o moi noi, con goc thi giong nhau.
## Nguyen nhan thu nhat: doc lai 70 file o moi request
Ham nap ban dich trong nhu the nay, kieu code AI viet rat tu nhien:
```ts
const cache = new Map()
async function load(locale: string) {
const hit = cache.get(locale)
if (hit && process.env.NODE_ENV === 'production') return hit // chi dung cache khi chay that
const out = await readAllFiles(locale) // doc ~70 file
cache.set(locale, out)
return out
}
```
Chu `production` co ly do: luc phat trien ban sua mot cau chu va muon thay ngay, nen cache bi tat. Hau qua la o dev, moi request deu doc lai 70 file. Do bang sampling heap profiler thi cac muc bi giu nhieu nhat la ham doc ban dich va ham chup stack trace ma runtime dev cua Next/React goi cho tung promise, ke ca promise cua `readFile`. Noi gon: ban dev ghi lai dau vet moi lan doc, va 70 lan moi request thi dau vet chat dong.
Cach sua gom hai thay doi nho. Cache chinh promise, khong doi co ket qua moi cache, de muoi request dong thoi cung cho mot lan doc:
```ts
const cache = new Map<string, Promise<Messages>>()
function load(locale: string) {
let hit = cache.get(locale)
if (!hit) {
hit = readAllFiles(locale)
hit.catch(() => cache.delete(locale)) // loi thi lan sau thu lai
cache.set(locale, hit)
}
return hit
}
```
Va giu tien loi cua dev bang cach xoa cache khi file doi, thay cho viec tat han cache:
```ts
if (process.env.NODE_ENV !== 'production') {
watch(messagesDir, { recursive: true }, () => cache.clear()).unref()
}
```
Da kiem tra: sua mot file ban dich thi trang doi ngay, tra lai thi ve nhu cu.
## Nguyen nhan thu hai: loi rieng cua Next.js dev
Sau ban sua, server sach ban rieng trang `/lien-he` van thay heap sau GC tang theo duong thang: 290, 628, 967, 1.303 MB, moi moc 500 request. Mot trang admin trong chi co root layout cung tang khoang 1,2 MB moi request.
Nguoi dieu tra dung mot app Next trong, khong co dong code nao cua du an. Root layout goi `await headers()`, trang chi co mot the h1:
```jsx
// app/layout.jsx
import { headers } from 'next/headers'
export default async function L({ children }) {
await headers()
return <html><body>{children}</body></html>
}
// app/page.jsx
export default function P() { return <h1>hi</h1> }
```
Chay `next dev`, goi `/` 1.500 lan, do heap sau khi ep GC:
| Phien ban Next | Heap sau 1.500 request |
|---|---|
| 16.4.2 | 90 → 343–350 MB |
| 16.4.3 | 82 → 334 MB |
| 16.5.0-canary.27 | 80 → 335 MB |
Doi chung tren cung app, moi lan 1.500 request:
| Thay doi | Ket qua |
|---|---|
| `await connection()` trong layout thay cho `headers()` | 91 → 109 MB |
| Khong goi API dong nao | 88 → 126 MB |
| `headers()` dat trong server component con boc `children` | van ro (90 → 348 MB) |
Muc ro con tang theo kich thuoc du lieu ma trang gui xuong trinh duyet. App nay dang dua toan bo khoang 240 KB ban dich xuong moi trang qua `NextIntlClientProvider`. Bo phan do thi muc ro tu 1,2 xuong khoang 0,43 MB moi request. O app trong, payload lon nhung khong co `headers()` thi khong ro.
Minh chua thay issue tuong ung o kho Next.js, va nguoi dieu tra chua gui. Neu ban gap dung to hop nay, day la kich ban tai hien nho nhat.
## Y nghia
Hai nguyen nhan cong lai. Loi cua Next lam moi request de lai mot it. Doan code doc 70 file lam cai "mot it" do to ra nhieu lan. Sua cache ban dich keo con so tu 9,8 GB xuong muc chap nhan duoc cho mot buoi lam viec, nhung khong dua no ve phang nhu production.
Hai bai hoc nho:
- Truoc khi do loi cho code cua minh, hoac cho framework, hay dung mot app trong lam doi chung. Khong co doi chung thi chi co suy doan.
- Du lieu dung chung, it doi, dung doc lai o moi request, du ban dung server nao. Ban dich, cau hinh, danh muc, template email, prompt cua chatbot deu thuoc nhom nay. Cache promise, xoa cache dung luc du lieu doi.
## Kiem tra app cua ban
1. Chay dev, ghi RAM tien trinh server, roi goi mot trang 500 lan: `for n in $(seq 1 500); do curl -s -o /dev/null localhost:3000/trang-cua-ban; done`. Do lai bang `footprint -f bytes <pid>` (macOS) hoac `top`. Tang deu va khong ha la co van de.
2. Chay cung kich ban tren ban build chay that cua app. Neu ban do dung yen thi phan lon la chuyen cua che do dev, dung voi sua code chay that. Neu ban that cung tang thi loi nam o code cua ban.
3. Tim cac dieu kien kieu `NODE_ENV === 'production'` hay `DEBUG` dung canh mot cache, va cac `readFile`, `fetch`, truy van database nam trong code chay o moi request ma ket qua khong phu thuoc nguoi goi.
4. Rieng voi Next.js: neu layout goc cua ban goi `await headers()` hay `cookies()`, thu doi sang `await connection()` hoac chuyen phan can header xuong sau hon de xem bo nho co het tang khong. Chi do, chua chac ap dung duoc cho app cua ban.
5. Dung dua toan bo ban dich xuong trinh duyet khi trang chi dung vai nhom chu.