SydexaSydexa
Trang chủKhoá họcBài viết
Rò rỉ bộ nhớ - Sát thủ vô hình
Frontend

Rò rỉ bộ nhớ - Sát thủ vô hình

Hải Phương
|
18/11/2025
|
7 phút đọc

Có bao giờ bạn nhận được phản ánh từ khách hàng rằng trang web đang dùng mở ra vài phút là lại bị đơ, Task Manager của Chrome hiển thị memory tăng vọt lên 1.2GB. Vậy tại sao lại xảy ra vấn đề này?

Hãy thử xem qua đoạn mã này:

mounted() {

document.addEventListener('click', this.handleGlobalClick)

}

Bạn có thấy đoạn code vô hại này không? Chỉ là chúng ta đang lắng nghe sự kiện click thôi mà, có vấn đề gì ở đây nhỉ? Đó là việc thiếu đi removeEventListener , chúng ta chỉ gắn listener mà không xóa bỏ nó, rò rỉ bộ nhớ cứ thế âm thầm diễn ra và hạ gục trang web của bạn.

Vậy hãy cùng tôi tìm hiểu sâu hơn về vấn đề này nhé

I. “Ghost listener” trong biểu mẫu động

Chúng ta có hệ thống quản lý thông tin khách hàng cho phép người dùng thêm thẻ động. Mỗi thẻ đều lắng nghe sự kiện click toàn cục để tự đóng:

Quy trình thao tác của người dùng:

  1. Thêm 10 liên hệ → Tạo ra 10 trình lắng nghe sự kiện click
  2. Xóa tất cả → DOM thẻ bị gỡ bỏ, nhưng trình lắng nghe vẫn còn
  3. Lặp lại 5 lần → Tích lũy 50 trình lắng nghe "ma"

Mỗi lần click trên trang, 50 hàm này đều được thực thi. Tệ hơn - chúng đều giữ tham chiếu đến component đã bị hủy.

II. Tại sao không gỡ trình lắng nghe sự kiện lại gây rò rỉ bộ nhớ?

1. Trình nghe sự kiện không thể được thu hồi

Hãy nhìn qua sơ đồ quan hệ tham chiếu bộ nhớ:

Điểm then chốt:

  • addEventListener khiến phần tử đích (document) giữ tham chiếu đến hàm lắng nghe
  • Nếu hàm lắng nghe là phương thức của component, nó thường thông qua closure tham chiếu đến toàn bộ instance component
  • Ngay cả khi component bị hủy, DOM bị gỡ bỏ, chỉ cần trình nghe chưa bị xóa, GC (Garbage Collector) sẽ không dám thu hồi instance này

Đây chính là mô hình rò rỉ điển hình do "tham chiếu vòng + tham chiếu từ đối tượng gốc bên ngoài" gây ra.

2. Reachability của V8

JavaScript engine (như V8) sử dụng reachability để xác định có thu hồi object hay không:

  • Chỉ những object không thể truy cập được từ "đối tượng gốc" (root - như window, document) mới bị thu hồi.

Mà document.addEventListener() tương đương với việc gắn một chuỗi tham chiếu lên đối tượng gốc toàn cục document, khiến component đáng lẽ đã "chết" lại "sống lại từ cõi chết".

III. Bắt ma bằng Chrome DevTools

Mở Chrome DevTools→ Panel Memory → Take heap snapshot:

  1. Mở trang, thêm 3 thẻ liên hệ
  2. Xóa tất cả thẻ
  3. Kích hoạt thu gom rác thủ công (GC)
  4. Chụp nhanh heap lần nữa

Trong chế độ Comparison, bạn sẽ phát hiện:

  • Detached <div>: 3 phần tử đã tách khỏi DOM
  • Closure hoặc Function: 3 hàm xử lý sự kiện tương ứng
  • VueComponent: 3 instance component chưa được thu hồi

Đây chính là những "component ma" - chúng đã không còn trên trang nhưng vẫn chiếm dụng bộ nhớ.

IV. Cú pháp đúng: Lắng nghe sự kiện và hủy sự kiện phải đi cùng nhau

Ngoài ra bạn có thể sử dụng tùy chọn sự kiện { once: true } để tự động dọn dẹp:

Nhưng trong các trường hợp polling hoặc lắng nghe liên tục, việc quản lý thủ công vẫn là bắt buộc.

V. Ngoài event listener, còn những nguyên nhân gây rò rỉ bộ nhớ phổ biến nào khác?

1. Timer chưa được dọn dẹp (phổ biến nhất)

Chuỗi tham chiếu tương tự: window → setInterval → callback → component

2. Tham chiếu closure đến object lớn

Chỉ cần hàm process tồn tại, hugeData sẽ không bị thu hồi.

3. Tham chiếu DOM chưa được giải phóng

Ngay cả khi component bị hủy, globalRef vẫn trỏ đến DOM cũ, và các sự kiện, thuộc tính liên quan của nó không thể được dọn dẹp.

4. Sử dụng WeakMap/WeakSet không đúng cách

Bạn nghĩ WeakMap có thể tự động dọn dẹp? Sai! Chỉ có khóa (key) là object mới có tham chiếu yếu:

Cách dùng đúng là sử dụng instance component làm key:

VI. Các framework chính giúp chúng ta tránh rò rỉ bộ nhớ như thế nào?

Framework Cơ chế tự động dọn dẹp Trường hợp vẫn cần xử lý thủ công
Vue Sự kiện template (@click) tự động hủy addEventListener, setInterval
React useEffect trả về hàm dọn dẹp (tương tự beforeDestroy) Không return đúng hàm dọn dẹp
Angular @HostListener tự động hủy binding Thao tác DOM nguyên bản, callback thư viện bên thứ ba

Hãy nhớ: Framework chỉ có thể quản lý những sự kiện "nó biết". Một khi bạn sử dụng API nguyên bản, trách nhiệm lại thuộc về developer.

VII. Bonus: một vài cách xử lý vấn đề nâng cao

1. Rò rỉ callback từ SDK bên thứ ba

Như API bản đồ, trình phát video.

Giải pháp: Đóng gói instance SDK vào component, trong beforeDestroy gọi map.destroy() hoặc player.dispose()

2. Kết nối WebSocket chưa đóng

3. Texture Canvas/WebGL chưa được giải phóng

Tài nguyên đồ họa chiếm dụng trực tiếp bộ nhớ GPU. Cần thủ công gl.deleteTexture(), canvas.remove().

VIII. Checklist giúp bạn hạn chế viết code gây rò rỉ bộ nhớ

Mỗi khi viết code có thể gây rò rỉ, hãy tự hỏi:

✅ Có có hàm "dọn dẹp" tương ứng không?

✅ Chuỗi tham chiếu có kéo dài vòng đời object ngoài ý muốn không?

✅ Có thể tối ưu bằng WeakMap/WeakSet không?

✅ Có thể dùng { once: true } hoặc AbortController  để quản lý tự động không?

Tổng kết

Rò rỉ bộ nhớ không phải là vấn đề "có xảy ra hay không" mà là "khi nào nó bùng phát". Nó giống như bệnh mãn tính, giai đoạn đầu không có dấu hiệu, đến khi người dùng phàn nàn về độ trễ thì thường đã tích tụ quá lâu.

Lập trình viên frontend tốt không phải là người viết animation hào nhoáng, mà là người có thể nhìn thấy vòng đời tài nguyên tiềm ẩn trong từng dòng code.

Ngoài ra nếu muốn đào sâu hơn nữa, học các kiến thức chuyên sâu thực tiễn hơn từ giảng viên là chuyên gia có hơn 10 năm kinh nghiệm frontend và từng là kỹ sư tại Shopee Singapo thì bạn có thể tham khảo khoá học React Nâng Cao bên dưới của Sydexa nha!

React Nâng Cao - Chuyên Sâu với Tối Ưu Hiệu Năng
Khóa học React từ cơ bản đến nâng cao với chuyên gia. Trở thành Senior React Developer với Sydexa.
SydexaSydexa Team

Bài viết liên quan

Đừng đổ lỗi cho tối ưu hiệu năng nữa: Ứng dụng của bạn giật lag, có thể là do thiết kế sản phẩm quá tệ
Frontend
17/03/2026 · 8 phút đọc

Đừng đổ lỗi cho tối ưu hiệu năng nữa: Ứng dụng của bạn giật lag, có thể là do thiết kế sản phẩm quá tệ

Chắc hẳn mỗi khi chúng ta code xong một trang web và deploy lên production thì có một điều chúng ta khá quan tâm là Tối ưu hiệu năng (Performance Optimization). Trên mạng có nhiều bài viết chứa những thuật ngữ nghe rất đao to búa lớn: * Sử dụng Virtual List (danh sách ảo) để tối ưu việc render danh sách dài... * Sử dụng Web Worker để đẩy các tính toán phức tạp ra khỏi luồng chính (main thread)... * Sử dụng WASM để viết lại thuật toán lõi... Sau khi viết vài bài về tối ưu hiệu năng, cũng nh

Chi tiết bài viết
Chuyện gì thực sự xảy ra ở trình duyệt sau khi nhấn F5?
Frontend
11/03/2026 · 11 phút đọc

Chuyện gì thực sự xảy ra ở trình duyệt sau khi nhấn F5?

Trong lập trình frontend, "làm mới trang" trông có vẻ là một thao tác đơn giản, nhưng ẩn sau đó là một luồng phối hợp phức tạp giữa giao tiếp mạng (network communication), chiến lược cache (caching strategy) và engine render (rendering engine). Đặc biệt là khi nhấn F5, nó không phải là "tải lại hoàn toàn", cũng không phải là "dùng trực tiếp cache ở local", mà là một cơ chế xác thực thông minh nằm giữa hai trạng thái này. Để thực sự làm chủ các kỹ năng tối ưu hóa hiệu năng và debug frontend, bạn

Chi tiết bài viết
Server DTO: Nên giữ nguyên hay chuyển đổi?
Frontend
04/03/2026 · 12 phút đọc

Server DTO: Nên giữ nguyên hay chuyển đổi?

Trong các dự án mà Backend và Frontend được phát triển hoàn toàn tách biệt, chắc hẳn không ít lần chúng ta phải đau đầu với câu hỏi: "Dữ liệu server trả về thì nên xài luôn hay phải xào nấu lại?". Nếu dạo quanh các diễn đàn công nghệ hay đọc các bài thảo luận trên mạng, bạn sẽ thấy cuộc tranh luận về cách xử lý dữ liệu từ API này dường như chưa bao giờ có hồi kết. Cốt lõi của vấn đề luôn xoay quanh hai luồng quan điểm cực kỳ quen thuộc: "Dữ liệu backend trả về sao thì cứ tận dụng nguyên xi như

Chi tiết bài viết
Đừng lạm dụng useMemo nữa! Đây mới là Best Practice chuẩn xác dành cho bạn
Frontend
25/02/2026 · 12 phút đọc

Đừng lạm dụng useMemo nữa! Đây mới là Best Practice chuẩn xác dành cho bạn

useMemo là một React Hook giúp chúng ta ghi nhớ kết quả tính toán giữa các lần render để tối ưu hiệu năng trong một số trường hợp. Tuy nhiên, có một thực trạng đang diễn ra: người không biết thì chẳng bao giờ dùng, còn người đã biết thì lại lạm dụng nó ở mọi lúc, mọi nơi. Bài viết này sẽ giúp bạn hiểu rõ thực sự thì trong những trường hợp nào chúng ta mới nên sử dụng useMemo. Cú pháp tổng quát: useMemo hoạt động như thế nào? Ở lần render đầu tiên, useMemo sẽ thực thi hàm calculateValue (khôn

Chi tiết bài viết
Sydexa
Sydexa
SydexaSydexa

Nền tảng học lập trình với animation trực quan và bài tập tương tác, giúp bạn hiểu sâu thay vì chỉ đọc lý thuyết.

Sydexa FanpageBackend GroupFrontend Group

We learn, We share, We grow

Powered By
Cộng đồng System Design Việt Nam

contact@sydexa.com
0971489013