Tối ưu hiệu năng ứng dụng .NET: điểm nghẽn dev hay bỏ sót

Khi một ứng dụng .NET bắt đầu chậm, phản xạ đầu tiên của nhiều người là thêm cache hoặc nâng cấu hình server. Đôi khi cách đó có tác dụng, nhưng phần lớn thời gian, nguyên nhân thật nằm ở những chỗ rất nhỏ trong mã mà chúng tôi vẫn thấy lặp lại qua nhiều dự án. Bài này tổng hợp những điểm nghẽn như vậy và cách xử lý từng cái theo hướng dễ áp dụng.

Vì sao hiệu năng thường bị đánh giá sai

Đội ngũ của chúng tôi từng nhận một dự án API quản lý đơn hàng phản hồi chậm khoảng hai giây mỗi request. Team ban đầu nghi ngờ database và đã định đổi sang một instance lớn hơn. Khi đo bằng profiler, chúng tôi thấy phần lớn thời gian nằm ở việc serialize một danh sách lớn sang JSON, không phải ở truy vấn SQL. Chi phí nâng cấp server hoàn toàn không cần thiết.

Bài học rút ra khá đơn giản: đo trước, sửa sau. Nếu không có số liệu, bạn đang đoán. Visual Studio có sẵn công cụ Diagnostic Tools để xem CPU và bộ nhớ theo thời gian thực. Với môi trường production, các công cụ như dotnet-trace hay dotnet-counters cho cái nhìn sâu hơn.

Điểm nghẽn thứ nhất: truy vấn Entity Framework không kiểm soát

Entity Framework rất tiện, nhưng sự tiện lợi đó dễ che mất chi phí thực sự. Lỗi phổ biến nhất là N+1 query: bạn lấy một danh sách rồi duyệt qua từng phần tử để lấy dữ liệu liên quan. Mỗi vòng lặp sinh thêm một truy vấn.

Cách xử lý là tải dữ liệu liên quan ngay từ đầu bằng Include, hoặc dùng Select để chỉ lấy đúng cột cần thiết. Ví dụ:

var orders = await context.Orders
    .AsNoTracking()
    .Where(o => o.Status == OrderStatus.Pending)
    .Select(o => new OrderDto(o.Id, o.Total, o.Customer.Name))
    .ToListAsync();

Hai thay đổi ở đây có ý nghĩa riêng. AsNoTracking bỏ qua change tracker khi bạn chỉ đọc dữ liệu, giúp giảm bộ nhớ đáng kể. Select đưa phần lọc cột xuống tầng SQL, nên database chỉ trả về những gì ứng dụng cần. Với danh sách hàng chục nghìn bản ghi, khác biệt có thể lên tới nhiều lần.

Một lưu ý thực tế: đừng áp dụng AsNoTracking cho các thao tác cập nhật, vì khi đó bạn cần change tracker để EF biết cái gì đã thay đổi.

Điểm nghẽn thứ hai: đồng bộ chặn luồng

Trong ASP.NET Core, mỗi request chiếm một thread từ thread pool. Nếu bạn gọi .Result hoặc .Wait() trên một tác vụ bất đồng bộ, thread đó bị chặn trong khi chờ I/O. Dưới tải cao, thread pool cạn dần và hệ thống chậm đi dù CPU vẫn rảnh.

Quy tắc đơn giản là dùng async và await xuyên suốt từ controller đến tầng truy cập dữ liệu. Tránh trộn lẫn đồng bộ và bất đồng bộ trong cùng một luồng xử lý. Khi làm việc với I/O như gọi HTTP hay đọc file, hãy luôn ưu tiên phiên bản async của API.

Điểm nghẽn thứ ba: cấp phát bộ nhớ không cần thiết

Chuỗi là nguồn gây áp lực bộ nhớ phổ biến. Nối chuỗi bằng toán tử + trong vòng lặp tạo ra hàng loạt đối tượng tạm, khiến garbage collector phải làm việc nhiều hơn.

Với những trường hợp ghép chuỗi lớn, StringBuilder là lựa chọn tốt hơn. Với dữ liệu lặp lại, hãy cân nhắc các kiểu như Span<T> và ArrayPool<T> để tái sử dụng bộ nhớ thay vì cấp phát mới liên tục. Đừng tối ưu những đoạn chạy một lần khi khởi động; hãy tập trung vào đường đi nóng, tức là những đoạn được gọi hàng nghìn lần mỗi giây.

Điểm nghẽn thứ tư: cache dùng sai cách

Cache giải quyết được nhiều vấn đề, nhưng cache sai có thể gây ra lỗi khó lường. Một lỗi hay gặp là cache mọi thứ với thời gian sống quá dài, dẫn đến dữ liệu cũ và bộ nhớ phình to. Một lỗi khác là cache những dữ liệu vốn thay đổi liên tục, khiến chi phí quản lý cache lớn hơn lợi ích.

Trước khi thêm cache, hãy tự hỏi ba câu: dữ liệu này được đọc nhiều hơn ghi không, nó có thể chấp nhận trễ bao lâu, và chi phí tính lại nó có đủ lớn không. Nếu câu trả lời không rõ ràng, có lẽ bạn chưa cần cache.

Một số kiểm tra nhanh trước khi phát hành

Trước mỗi lần release, chúng tôi thường kiểm tra vài điểm sau:

  • Có truy vấn nào sinh ra trong vòng lặp không, và đã dùng Include hoặc Select chưa?
  • Có chỗ nào chặn luồng bằng .Result hay .Wait() không?
  • Các kết quả chỉ đọc đã dùng AsNoTracking chưa?
  • Dữ liệu cache có thời gian sống hợp lý và có cơ chế xóa không?

Không cần phải làm tất cả ngay từ đầu. Điều quan trọng là có một quy trình đo lường lặp lại được, để mỗi lần tối ưu đều dựa trên số liệu. Nếu bạn muốn tìm hiểu thêm về những sai lầm thường gặp khi làm việc với backend, bạn có thể đọc Top 10 sai lầm thường gặp về backend development khi làm .NET, và phần nói về truy vấn đọc thêm trong Top 10 sai lầm thường gặp về Entity Framework.

Đo lường trước, sửa sau

Tối ưu hiệu năng ứng dụng .NET là một quá trình liên tục hơn là một lần sửa lớn. Những điểm nghẽn trong bài này xuất hiện rất thường xuyên, nhưng mỗi dự án có đặc thù riêng. Thói quen tốt nhất là đo đạc, xác định nút thắt thật sự, sửa đúng chỗ đó và đo lại để xác nhận. Khi bạn đã quen với quy trình này, các quyết định về hạ tầng và kiến trúc cũng sẽ dễ đưa ra hơn nhiều.

Nếu bạn đang làm việc với các bài toán tích hợp AI trong hệ thống, phần về cách thiết kế API layer để dữ liệu chạy mượt cũng liên quan trực tiếp đến hiệu năng, bởi lớp API tốt giúp giảm bớt các điểm nghẽn phát sinh khi số lượng yêu cầu tăng.