Xử gọn memory leak: Chọn Chrome DevTools hay MemLab?

Bộ nhớ trang web bỗng dưng phình to? Khám phá ngay cách truy vết và “bóc” memory leak cực nhanh bằng Chrome DevTools và MemLab - hai công cụ dev nào cũng nên biết!

Upload image

Dù là lập trình viên front-end lâu năm, nhiều người vẫn xem nhẹ vấn đề memory leak (rò rỉ bộ nhớ) cho đến khi người dùng than phiền vì trang chậm, tốn tài nguyên hoặc treo trình duyệt. Thực tế, memory leak là “thủ phạm thầm lặng” khiến web trở nên trì trệ. Những sự cố này thường làm giảm trải nghiệm web và ảnh hưởng xấu đến tỉ lệ người dùng quay lại.

Bài viết này sẽ giới thiệu cách phát hiện memory leak hiệu quả bằng hai công cụ mạnh: Chrome DevToolsMemLab.

Nguyên nhân gây memory leak trong JavaScript

Memory leak xảy ra khi vùng nhớ đã được cấp phát nhưng không thể thu hồi vì lỗi lập trình, dù dữ liệu đó không còn được sử dụng. Trong JavaScript, bộ thu gom rác (Garbage Collector) tự động giải phóng bộ nhớ khi dữ liệu không cần thiết nữa. Tuy nhiên, nếu có biến giữ lại tham chiếu đến dữ liệu không cần thiết, bộ nhớ sẽ không được giải phóng, dẫn đến tích tụ và giảm hiệu suất ứng dụng gây ra các vấn đề như chạy chậm, lag hay crash máy. Mặc dù JavaScript có bộ thu gom rác, memory leak vẫn thường gặp trong các ứng dụng phức tạp, vì vậy cần kiểm tra thường xuyên để duy trì hiệu năng và độ ổn định.

Vậy làm sao để phát hiện và xử lý memory leak hiệu quả? Đây là lúc chúng ta cần đến các công cụ phân tích bộ nhớ.

Giải pháp phát hiện memory leak hiệu quả

Chrome DevTools là lựa chọn phổ biến nhờ khả năng phân tích chi tiết và truy vết thủ công. Tuy nhiên, thao tác thủ công thường mất thời gian và khó lặp lại. Để khắc phục hạn chế này, chúng ta có MemLab từ nhà Meta – một giải pháp mang đến sự tự động hóa toàn bộ quy trình từ mô phỏng hành vi người dùng đến phân tích bộ nhớ, giúp phát hiện rò rỉ mà không cần thao tác thủ công. Kết hợp cả hai công cụ sẽ giúp developer kiểm tra memory leak một cách toàn diện, từ phân tích chuyên sâu đến kiểm thử lặp lại, phù hợp trong cả quá trình phát triển lẫn tích hợp CI/CD.

Chrome DevTools - Bộ đồ nghề săn memory leak thủ công

Chrome DevTools là công cụ không thể thiếu đối với mọi front-end developer, đặc biệt khi cần phân tích và xử lý memory leak. Trong đó có ba công cụ chính dùng để kiểm tra memory leak:

  • Tab Performance: Giúp theo dõi mức sử dụng bộ nhớ theo thời gian thực và phát hiện các pattern bất thường ở Heap, DOM nodes, và JS event listeners.

  • Tab Memory: Giúp phân tích sâu về các object được giữ lại không cần thiết và xác định nguyên nhân gây rò rỉ. Trong tab này có 2 công cụ chính để xử lý memory leak:

    • Allocations on timeline: Quan sát bộ nhớ cấp phát trong thời gian thực, giúp khoanh vùng các object đáng ngờ.
    • Heap snapshot: Cung cấp ảnh chụp snapshot tại các thời điểm cụ thể, hỗ trợ phân tích chi tiết và so sánh giữa các thời điểm.

Để kiểm tra memory leak hiệu quả, ta nên kết hợp cả ba công cụ này theo một quy trình hợp lý:

  1. Bắt đầu với Performance để đánh giá tổng quan.
  2. Sử dụng Heap snapshot để so sánh, phân tích chi tiết.

Ngoài ra, ta có thể dùng thêm Allocations on timeline trước bước sử dụng Heap snapshot để xác định thời điểm bộ nhớ được cấp phát nhiều và khoanh vùng các vị trí memory leak có thể xảy ra. Bước này là không bắt buộc nhưng sẽ giúp tiết kiệm thời gian hơn cho việc phân tích chi tiết.

Sau đây, ta sẽ tìm hiểu từng bước cách xử lý memory leak với Chrome DevTools.

Bước 1: Dùng Performance để đánh giá tổng quan

Bắt đầu với tab Performance - nơi có thể theo dõi tổng quan tình trạng bộ nhớ của trang web trong quá trình tương tác thực tế. Nó sẽ ghi lại toàn bộ hoạt động của trang trong một khoảng thời gian tương tác, bao gồm cả việc phân bổ bộ nhớ (JS Heap), số lượng DOM node, listeners, v.v.

Để sử dụng:

  1. Mở DevTools → Chọn tab “Performance”.

Upload image

  1. Click nút Record (●) và thực hiện các thao tác tương tác như người dùng thực tế trên trang.

Upload image

  1. Nhấn Stop để kết thúc quá trình ghi sau khi đã thực hiện xong các thao tác.

Upload image

  1. Kéo xuống dưới cùng của để xem các biểu đồ: JS Heap, Documents, Nodes, Listeners, v.v.

Upload image

Khi quan sát biểu đồ, ta nhận thấy các dấu hiện có thể là memory leak:

  • JS Heap tăng dần sau mỗi tương tác nhưng không giảm trở lại là dấu hiệu rõ rệt nhất. (Đường màu xanh dương)
  • DOM nodes hoặc Listeners tăng liên tục mà không bị giải phóng cũng là dấu hiệu cần chú ý. (Đường màu xanh lá cây và vàng)

Upload image

Bước 2: Sử dụng Heap snapshot để so sánh, phân tích chi tiết

Sau khi xác định đã xảy ra tình trạng memory leak, bước tiếp theo là đi sâu vào chi tiết để tìm xem object nào đang bị giữ lại. Lúc này ta sẽ cần đến tab Memory với tính năng Heap snapshot để xử lý. Heap snapshot giúp “chụp ảnh” bộ nhớ tại một thời điểm để xem những object nào còn tồn tại, và liệu có object nào đáng lý nên bị dọn đi nhưng vẫn còn đó hay không.

Cách làm như sau:

  1. Mở tab Memory → Chọn Heap snapshot → Click Take snapshot.

Upload image

  1. Sau khi snapshot được tạo, dùng dropdown ở góc trái để chọn Comparison khi đã chụp đủ snapshot (trước và sau thao tác).

Ở chế độ Comparison, chúng ta có thể so sánh hai snapshot (trước và sau thao tác người dùng) để phát hiện sự thay đổi trong bộ nhớ. Mỗi dòng trong bảng thể hiện các object đã xuất hiện, biến mất hoặc thay đổi giữa hai thời điểm. Các cột DeltaSize Delta giúp xác định object nào tăng bất thường – dấu hiệu tiềm tàng của memory leak.

Upload image

Upload image

Ví dụ dưới đây cho thấy một dòng Array object có Delta cao bất thường. Khi mở rộng, ta thấy mảng này chứa nhiều object giống nhau (~80kB mỗi object).

Upload image

Dựa theo retainer trace, có thể thấy các object này được giữ lại bởi một biến có tên là leakyList, được sử dụng trong hàm MemoryLeakDemo(). Giả sử leakyListbiến toàn cục dùng làm cache sản phẩm đã thêm vào giỏ. Tuy nhiên, mặc dù giỏ chỉ chứa 50 sản phẩm, mảng lại có hơn 140 phần tử – cho thấy object cũ không được giải phóng khi giỏ bị xóa, gây nên memory leak.

Upload image

Ví dụ này minh họa cách sử dụng Heap snapshot để lần theo dấu vết của các object bị giữ lại trong bộ nhớ, từ đó giúp xác định và khoanh vùng nguyên nhân gây ra memory leak trong ứng dụng.

Ngoài ra, Một dạng leak phổ biến khác là Detached DOM tree – node DOM bị xóa khỏi giao diện nhưng vẫn tồn tại trong bộ nhớ. Có thể tìm kiếm nhanh với từ khóa "Detached" trên thanh tìm kiếm để xác định, sau đó lần ngược theo retainer trace để xử lý.

Upload image

Bonus: Dùng Allocations on timeline để khoanh vùng object đáng ngờ

Để hỗ trợ việc phân tích Heap Snapshot một cách dễ dàng và hiệu quả hơn, tính năng Allocations on timeline trong Chrome DevTools cho phép bạn ghi lại các object được tạo mới trong một khoảng thời gian cụ thể, từ đó giúp nhanh chóng phát hiện các cấp phát bộ nhớ bất thường có thể liên quan đến memory leak.

Cách sử dụng như sau:

  1. Trong tab “Memory”, chọn Allocation instrumentation on timeline.

Upload image

  1. Nhấn Start và thực hiện tương tác trên trang.

Upload image

  1. Nhấn Stop (●) để kết thúc quá trình ghi sau khi đã hoàn thành tương tác.

Upload image

Sau khi dừng ghi, một biểu đồ xuất hiện, thể hiện các object được cấp phát tại từng thời điểm, mỗi cột màu xanh biểu thị một đợt cấp phát bộ nhớ đã diễn ra.

Upload image

Tính năng cho phép chúng ta có thể kéo để chọn một khoảng thời gian cụ thể, đặc biệt tại các đoạn có dấu hiệu tăng đột biến bộ nhớ, để thu hẹp phạm vi phân tích. Khi đó, DevTools sẽ hiển thị danh sách các object được tạo ra trong khoảng thời gian đó, kèm theo stack trace tương ứng, cho phép dễ dàng truy vết đến đoạn code đã tạo ra object.

Upload image

MemLab - Tự động hóa truy vết memory leak

MemLab giúp kiểm thử memory leak một cách có hệ thống và có thể tự động hóa tốt. Thay vì phải kiểm tra thủ công và khó tái lập, MemLab cho phép viết kịch bản mô phỏng hành vi người dùng (test scenario) bằng Puppeteer API và phân tích để phát hiện rò rỉ bộ nhớ theo cách định hướng rõ ràng và lặp lại được.

Các tính năng chính gồm có:

  • Tự động snapshot: So sánh bộ nhớ trước và sau tương tác người dùng
  • Trace rõ ràng: Hiển thị chuỗi giữ reference để xác định nguyên nhân leak
  • Gom cụm object: Tự động nhóm các object rò rỉ giúp phân tích dễ hơn

Ngoài ra còn có các tính năng nâng cao khác như:

  • Cho phép tự định nghĩa logic để đánh dấu object nào là leak
  • Tùy chỉnh thuật toán gom cụm
  • Tích hợp với test framework khác ngoài dùng Puppeteer như là Playwright hoặc Cypress

Tìm hiểu thêm về các tính năng của MemLab tại đây.

Cách hoạt động

MemLab phát hiện memory leak theo mô hình ba bước:

  1. Baseline: Truy cập vào trạng thái ban đầu (ví dụ: trang Home) và chụp heap snapshot đầu làm cơ sở so sánh.
  2. Target Interaction: Thực hiện thao tác mục tiêu (mở widget, điều hướng,…), sau đó chụp snapshot thứ hai.
  3. Revert: Hoàn tác thao tác ở bước 2 để trở về trạng thái ban đầu và chụp snapshot cuối cùng.

MemLab sẽ so sánh các snapshot để tìm các đối tượng được tạo ra ở bước 2. Những đối tượng này có mặt trong snapshot bước 2 nhưng không có trong snapshot đầu tiên, và vẫn tồn tại ở bước 3 - nghĩa là chúng không bị xóa như mong đợi. Đây là những đối tượng nghi ngờ có thể gây ra memory leak.

Upload image

Ví dụ minh họa

Phần demo chức năng giỏ hàng sẽ tiếp tục được sử dụng để kiểm thử với MemLab. Giả sử, bạn nghi ngờ có memory leak khi người dùng thêm và xóa sản phẩm, một số object vẫn bị giữ lại trong bộ nhớ thay vì được giải phóng. Để kiểm chứng điều này, hãy cùng viết một kịch bản kiểm thử.

  1. Viết kịch bản kiểm thử:
// Hàm phụ trợ: tìm và click vào một nút theo nội dung chữ hiển thị (text).
async function clickButtonByText(page, text) {
  // Tìm nút cần click vào bằng text (syntax của Puppeteer)
  const elements = await page.$$(`xpath/.//button[contains(., '${text}')]`);
  const [button] = elements;
  if (button) {
    await button.click();
  }
  await Promise.all(elements.map((e) => e.dispose())); // giải phóng các DOM handle
}

// Hàm `action`: định nghĩa các thao tác tương tác với trang – đây là bước kiểm tra bộ nhớ chính.
// Trong ví dụ này, ta mô phỏng người dùng thêm 50 sản phẩm vào giỏ và sau đó xóa giỏ – lặp lại 5 lần.
// Mục tiêu là kiểm tra xem có rò rỉ bộ nhớ sau các thao tác lặp lại này hay không.
async function action(page) {
  for (let i = 0; i < 5; i++) {
    await clickButtonByText(page, "Add 50 Products"); // thao tác thêm sản phẩm
    await new Promise((resolve) => setTimeout(resolve, 1000)); // đợi để trang xử lý DOM

    await clickButtonByText(page, "Clear Cart"); // thao tác xóa toàn bộ giỏ hàng
    await new Promise((resolve) => setTimeout(resolve, 1000)); // đợi để DOM ổn định
  }
}

// Hàm `back`: định nghĩa hành vi hoàn tác, để quay trở lại trạng thái ban đầu của app.
// Mục tiêu là giúp MemLab kiểm tra xem các object đã được cấp phát trong bước `action` có bị giải phóng không.
// Trong ví dụ này, trở về trang chính là cách để kết thúc tương tác và "dọn dẹp" memory.
async function back(page) {
  await page.click('a[href="/"]'); // click vào đường dẫn dẫn về trang chủ
}

// Hàm `url`: chỉ định URL của trang baseline – trạng thái ban đầu của app trước khi tương tác.
function url() {
  return "http://localhost:3000/examples/full-example"; // có thể điều chỉnh tùy theo định tuyến của Nuxt
}

// Các hàm `url`, `action`, `back` – cấu trúc kịch bản kiểm thử chuẩn cho MemLab
module.exports = {
  url,
  action,
  back,
};
  1. Chạy lệnh:
memlab run --scenario cart-memory-leak.js --work-dir ./
  • memlab run: Lệnh để khởi chạy MemLab (xem thêm tại đây)
  • --scenario ./cart-memory-leak.js: Chỉ định file kịch bản kiểm thử
  • --work-dir ./: Chỉ định thư mục làm việc nơi MemLab lưu các heap snapshot, file tạm và kết quả phân tích
  1. Kết quả và phân tích:

Sau khi chạy kiểm thử bằng MemLab, kết quả được hiển thị dưới dạng biểu đồ và trace các đối tượng nghi ngờ bị rò rỉ.

Upload image

Tóm tắt kết quả có được:

  • MemLab đã phát hiện 1 memory leak
  • Tổng dung lượng các object bị giữ lại: ~366KB
  • MemLab nhóm các object có trace giống nhau thành một cluster để phân tích – trong ví dụ này chỉ có 1 cluster (2000 object), nghĩa là tất cả leak đều cùng một nguồn gốc
  • Trace chính: Biến leakyDOM giữ một mảng Array chứa các Detached HTMLDivElement

Dựa vào các thông tin trên, chúng ta có thể kết luận đã xảy ra memory leak. Như vậy, chỉ với một kịch bản và một lệnh, MemLab đã tự động phát hiện memory leak một cách hiệu quả.

Một số lưu ý

Dù hỗ trợ tự động hóa, MemLab vẫn có một số hạn chế cần lưu ý:

  • Không phát hiện leak object bị giữ lại có chủ đích (cache chẳng hạn): MemLab chỉ đánh dấu một object là leak nếu:

    • Nó được cấp phát trong action()
    • Không bị giải phóng sau back()
    • Là một DOM node bị detached

    Để phát hiện hay đánh dấu thêm các object bị cho là leak, cần phải sử dụng leakFilter – một callback cho phép tự định nghĩa các quy tắc xác định memory leak theo nhu cầu cụ thể.

  • Chỉ hỗ trợ tốt với React: MemLab có tích hợp sẵn cho React. Với các framework khác (Vue, Angular…), chúng ta phải tự viết logic detect leak – điều này khá phức tạp do thiếu tài liệu.

  • Gom cụm đôi khi không chính xác: Kết quả mặc định có thể gom các object không liên quan vào chung một cluster lớn, gây khó phân tích. Điều này có thể cải thiện bằng cách dùng các tùy chọn như --ml-clustering, --ml-linkage-max-dist, --ml-clustering-max-df để chia nhỏ cụm và làm trace rõ ràng hơn. Ví dụ như trong trường hợp sử dụng leak filter này:

    //...... logic của các phần khác ở demo trước
    
    // Hàm leakFilter lọc các đối tượng (node) có kích thước bộ nhớ giữ lại lớn hơn 50KB hoặc bị detached
    function leakFilter(node, _snapshot, _leakedNodeIds) {
    const isLarge = node.retainedSize > 50 * 1000;
    const isDetached = node.is_detached === true;
    return isLarge || isDetached;
    }
    
    // Các hàm `url`, `action`, `back` – cấu trúc kịch bản kiểm thử chuẩn cho MemLab
    module.exports = {
    url,
    action,
    leakFilter,
    back,
    };
    

    Kết quả khi chạy mặc định có thể gom nhiều node không liên quan vào một cụm lớn, khiến trace không đầy đủ. Tuy nhiên, khi sử dụng --ml-clustering, --ml-linkage-max-dist hay --ml-clustering-max-df, cụm lớn sẽ được chia thành các cụm nhỏ hơn, giúp trace chính xác hơn.

Upload image

Kết luận

Vậy là chúng ta đã đi qua hai công cụ – một thủ công và một tự động – cùng với cách dùng và các tính năng nổi bật của chúng. Chrome DevTools là công cụ thủ công mạnh mẽ, cung cấp cái nhìn trực quan và chi tiết để điều tra memory leak ngay trong trình duyệt. Ngược lại, MemLab là công cụ tự động hóa, hỗ trợ kiểm tra memory leak lặp đi lặp lại qua test script và dễ dàng tích hợp vào CI/CD.

Mỗi công cụ đều có điểm mạnh riêng, phù hợp với các mục đích khác nhau trong quá trình tối ưu bộ nhớ. Sử dụng hiệu quả cả hai sẽ giúp việc phát hiện và xử lý memory leak trở nên chính xác, hiệu quả và tiết kiệm thời gian hơn.

Atekco - Home for Authentic Technical Consultants
Atekco on Apple Podcast