Bất kể bạn đang phát triển loại ứng dụng nào, việc tối ưu hoá hiệu suất và đảm bảo ứng dụng tải nhanh và mang lại các tương tác mượt mà là rất quan trọng đối với trải nghiệm người dùng và sự thành công của ứng dụng. Có một cách để thực hiện việc này là kiểm tra hoạt động của ứng dụng bằng cách sử dụng các công cụ phân tích tài nguyên để xem điều gì đang xảy ra khi ứng dụng đó chạy trong một khoảng thời gian. Bảng điều khiển Hiệu suất trong Công cụ cho nhà phát triển là một công cụ phân tích hiệu quả giúp phân tích và tối ưu hoá hiệu suất của các ứng dụng web. Nếu ứng dụng của bạn đang chạy trong Chrome, ứng dụng sẽ cung cấp cho bạn thông tin tổng quan chi tiết về những hoạt động mà trình duyệt đang thực hiện khi ứng dụng của bạn được thực thi. Hiểu hoạt động này có thể giúp bạn xác định các mẫu, nút thắt cổ chai và các điểm nóng về hiệu suất mà bạn có thể hành động để cải thiện hiệu suất.
Ví dụ sau đây hướng dẫn bạn cách sử dụng bảng điều khiển Hiệu suất.
Thiết lập và tạo lại tình huống lập hồ sơ
Gần đây, chúng tôi đặt mục tiêu làm cho bảng điều khiển Hiệu suất hoạt động hiệu quả hơn. Cụ thể, chúng tôi muốn trang này tải một lượng lớn dữ liệu về hiệu suất nhanh hơn. Ví dụ: đó là khi phân tích tài nguyên các quy trình chạy trong thời gian dài hoặc phức tạp hoặc khi thu thập dữ liệu có độ chi tiết cao. Để đạt được điều này, trước tiên, bạn cần hiểu rõ cách ứng dụng hoạt động và lý do ứng dụng hoạt động theo cách đó. Bạn cần thực hiện việc này bằng cách sử dụng công cụ phân tích tài nguyên.
Như bạn có thể đã biết, Công cụ cho nhà phát triển là một ứng dụng web. Do đó, bạn có thể lập hồ sơ dữ liệu bằng bảng Hiệu suất. Để phân tích tài nguyên bảng điều khiển này, bạn có thể mở Công cụ cho nhà phát triển, sau đó mở một phiên bản Công cụ cho nhà phát triển khác được đính kèm với bảng điều khiển đó. Tại Google, cách thiết lập này được gọi là DevTools-on-DevTools.
Sau khi thiết lập xong, bạn phải tạo lại và ghi lại tình huống cần lập hồ sơ. Để tránh nhầm lẫn, cửa sổ Công cụ cho nhà phát triển ban đầu sẽ được gọi là "phiên bản Công cụ cho nhà phát triển đầu tiên" và cửa sổ đang kiểm tra phiên bản đầu tiên sẽ được gọi là "phiên bản thứ hai trong Công cụ cho nhà phát triển".
![Ảnh chụp màn hình một thực thể của Công cụ cho nhà phát triển đang kiểm tra các phần tử trong chính Công cụ cho nhà phát triển.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-1.png?authuser=9&hl=vi)
Trên thực thể thứ hai của Công cụ cho nhà phát triển, bảng điều khiển Hiệu suất (được gọi là bảng điều khiển hiệu suất từ đây về sau) sẽ quan sát thực thể đầu tiên của Công cụ cho nhà phát triển để tạo lại tình huống tải hồ sơ.
Trên phiên bản Công cụ cho nhà phát triển thứ hai, một bản ghi trực tiếp sẽ bắt đầu, trong khi ở phiên bản đầu tiên, một hồ sơ được tải từ một tệp trên ổ đĩa. Một tệp lớn sẽ được tải để phân tích chính xác hiệu suất xử lý dữ liệu đầu vào lớn. Khi cả hai phiên bản tải xong, dữ liệu phân tích hiệu suất (thường được gọi là dấu vết) sẽ hiển thị trong phiên bản Công cụ thứ hai của bảng điều khiển hiệu suất đang tải hồ sơ.
Trạng thái ban đầu: xác định các cơ hội cải thiện
Sau khi tải xong, bạn có thể quan sát thấy nội dung sau đây trên phiên bản bảng điều khiển hiệu suất thứ hai trong ảnh chụp màn hình tiếp theo. Tập trung vào hoạt động của luồng chính hiển thị trong kênh có nhãn Main. Có thể thấy rằng có năm nhóm hoạt động lớn trong biểu đồ hình ngọn lửa. Những nhiệm vụ này bao gồm các tác vụ mất nhiều thời gian tải nhất. Tổng thời gian của các tác vụ này là khoảng 10 giây. Trong ảnh chụp màn hình sau đây, bảng hiệu suất được dùng để tập trung vào từng nhóm hoạt động trong số này để xem bạn có thể tìm thấy những gì.
![Ảnh chụp màn hình bảng điều khiển hiệu suất trong Công cụ cho nhà phát triển đang kiểm tra quá trình tải một dấu vết hiệu suất trong bảng điều khiển hiệu suất của một thực thể khác trong Công cụ cho nhà phát triển. Thời gian tải hồ sơ khoảng 10 giây. Thời gian này chủ yếu được chia thành 5 nhóm hoạt động chính.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-2.png?authuser=9&hl=vi)
Nhóm hoạt động đầu tiên: công việc không cần thiết
Rõ ràng là nhóm hoạt động đầu tiên là mã cũ vẫn chạy nhưng không thực sự cần thiết. Về cơ bản, mọi thứ trong khối màu xanh lục có nhãn processThreadEvents
đều là lãng phí công sức. Đó là một chiến thắng nhanh chóng. Thao tác xoá lệnh gọi hàm đó giúp tiết kiệm khoảng 1,5 giây. Tuyệt!
Nhóm hoạt động thứ hai
Trong nhóm hoạt động thứ hai, giải pháp không đơn giản như giải pháp đầu tiên. buildProfileCalls
mất khoảng 0, 5 giây và đó không phải là việc có thể tránh được.
![Ảnh chụp màn hình bảng điều khiển hiệu suất trong Công cụ cho nhà phát triển đang kiểm tra một thực thể khác của bảng điều khiển hiệu suất. Một tác vụ liên kết với hàm buildProfileCall mất khoảng 0,5 giây.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-3.png?authuser=9&hl=vi)
Vì tò mò nên chúng tôi đã bật tuỳ chọn Memory (Bộ nhớ) trong bảng hiệu suất để điều tra thêm và nhận thấy hoạt động buildProfileCalls
cũng đang sử dụng rất nhiều bộ nhớ. Ở đây, bạn có thể thấy biểu đồ đường màu xanh dương đột ngột thay đổi trong khoảng thời gian buildProfileCalls
chạy. Điều này cho thấy khả năng rò rỉ bộ nhớ.
![Ảnh chụp màn hình trình phân tích bộ nhớ trong Công cụ cho nhà phát triển đánh giá mức tiêu thụ bộ nhớ của bảng điều khiển hiệu suất. Trình kiểm tra cho thấy hàm buildProfileCallback chịu trách nhiệm về sự cố rò rỉ bộ nhớ.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-4.png?authuser=9&hl=vi)
Để theo dõi nghi ngờ này, chúng tôi đã sử dụng bảng điều khiển Bộ nhớ (một bảng điều khiển khác trong Công cụ cho nhà phát triển, khác với ngăn Bộ nhớ trong bảng điều khiển hiệu suất) để điều tra. Trong bảng điều khiển Bộ nhớ, mục "Lấy mẫu phân bổ" đã chọn, loại hồ sơ này sẽ ghi lại ảnh chụp nhanh vùng nhớ khối xếp cho bảng điều khiển hiệu suất khi tải hồ sơ CPU.
![Ảnh chụp màn hình trạng thái ban đầu của trình phân tích bộ nhớ. Lệnh 'lấy mẫu phân bổ' được đánh dấu bằng một hộp màu đỏ và cho biết rằng tuỳ chọn này phù hợp nhất để phân tích bộ nhớ JavaScript.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-5.png?authuser=9&hl=vi)
Ảnh chụp màn hình sau đây cho thấy ảnh chụp nhanh của vùng nhớ khối xếp đã được thu thập.
![Ảnh chụp màn hình trình phân tích bộ nhớ, trong đó thao tác dựa trên Set cần nhiều bộ nhớ được chọn.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-6.png?authuser=9&hl=vi)
Qua ảnh chụp nhanh của vùng nhớ khối xếp này, bạn có thể thấy lớp Set
đang chiếm nhiều bộ nhớ. Khi kiểm tra các điểm gọi, chúng ta nhận thấy rằng chúng ta đã không cần thiết chỉ định các thuộc tính thuộc kiểu Set
cho các đối tượng được tạo với khối lượng lớn. Chi phí này ngày càng gia tăng và tiêu tốn rất nhiều bộ nhớ, đến mức ứng dụng thường gặp sự cố khi sử dụng đầu vào lớn.
Tập hợp rất hữu ích trong việc lưu trữ các mục duy nhất và cung cấp các thao tác sử dụng tính duy nhất của nội dung, như loại bỏ tập dữ liệu trùng lặp và giúp tra cứu hiệu quả hơn. Tuy nhiên, những tính năng đó không cần thiết vì dữ liệu được lưu trữ được đảm bảo là dữ liệu riêng biệt từ nguồn. Do đó, ban đầu, các tập hợp không cần thiết. Để cải thiện quá trình phân bổ bộ nhớ, loại thuộc tính đã được thay đổi từ Set
thành một mảng thuần tuý. Sau khi áp dụng thay đổi này, hệ thống đã chụp một ảnh chụp nhanh của vùng nhớ khối xếp khác và quan sát thấy mức phân bổ bộ nhớ giảm. Mặc dù không đạt được những cải thiện đáng kể về tốc độ nhờ thay đổi này, nhưng lợi ích thứ hai là ứng dụng ít gặp sự cố hơn.
![Ảnh chụp màn hình trình phân tích bộ nhớ. Thao tác dựa trên Tập hợp cần nhiều bộ nhớ trước đây đã được thay đổi để sử dụng một mảng thuần tuý, giúp giảm đáng kể chi phí bộ nhớ.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-7.png?authuser=9&hl=vi)
Nhóm hoạt động thứ ba: cân nhắc ưu nhược điểm của cấu trúc dữ liệu
Phần thứ ba rất đặc biệt: bạn có thể thấy trong biểu đồ hình ngọn lửa phần này bao gồm các cột hẹp nhưng cao, biểu thị các lệnh gọi hàm sâu và đệ quy sâu trong trường hợp này. Tổng cộng, phần này kéo dài khoảng 1,4 giây. Bằng cách xem xét cuối phần này, rõ ràng chiều rộng của các cột này được xác định theo thời lượng của một hàm: appendEventAtLevel
, điều này cho thấy đây có thể là một nút thắt cổ chai
Trong quá trình triển khai hàm appendEventAtLevel
, có một điều nổi bật. Đối với mỗi mục dữ liệu riêng lẻ trong dữ liệu đầu vào (còn gọi là "sự kiện"), một mục đã được thêm vào bản đồ để theo dõi vị trí dọc của các mục trong dòng thời gian. Đây là một vấn đề vì số lượng mục được lưu trữ quá lớn. Maps nhanh chóng cho việc tra cứu dựa trên phím, nhưng lợi thế này không phải là miễn phí. Chẳng hạn, khi bản đồ ngày càng lớn hơn, việc thêm dữ liệu vào bản đồ có thể trở nên tốn kém do phải băm lại. Chi phí này trở nên dễ nhận thấy khi một số lượng lớn các mục được thêm vào bản đồ một cách liên tiếp.
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
Chúng tôi đã thử nghiệm một phương pháp khác không yêu cầu thêm một mục vào bản đồ cho mỗi mục nhập trong biểu đồ ngọn lửa. Sự cải thiện này rất đáng kể, xác nhận rằng nút thắt cổ chai thực sự liên quan đến chi phí phải chịu khi thêm tất cả dữ liệu vào bản đồ. Thời gian mà nhóm hoạt động giảm từ khoảng 1,4 giây xuống còn khoảng 200 mili giây.
Trước:
![Ảnh chụp màn hình bảng điều khiển hiệu suất trước khi thực hiện tối ưu hoá cho hàm appendEventAtlevel. Tổng thời gian để chạy hàm này là 1.372,51 mili giây.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-9.png?authuser=9&hl=vi)
Sau:
![Ảnh chụp màn hình bảng điều khiển hiệu suất sau khi thực hiện các bước tối ưu hoá cho hàm appendEventAtlevel. Tổng thời gian chạy hàm là 207,2 mili giây.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-10.png?authuser=9&hl=vi)
Nhóm hoạt động thứ tư: trì hoãn công việc không quan trọng và lưu dữ liệu vào bộ nhớ đệm để tránh công việc trùng lặp
Phóng to cửa sổ này, bạn có thể thấy rằng có hai khối lệnh gọi hàm gần giống hệt nhau. Bằng cách xem tên của các hàm được gọi, bạn có thể suy luận rằng các khối này bao gồm mã xây dựng cây (ví dụ: có các tên như refreshTree
hoặc buildChildren
). Trên thực tế, mã liên quan là mã tạo chế độ xem dạng cây trong ngăn dưới cùng của bảng điều khiển. Điều thú vị là các chế độ xem dạng cây này không hiển thị ngay sau khi tải. Thay vào đó, người dùng cần chọn một chế độ xem dạng cây (các thẻ "Từ dưới lên", "Cây cuộc gọi" và "Nhật ký sự kiện" trong ngăn) để hiển thị cây. Hơn nữa, như bạn có thể thấy từ ảnh chụp màn hình, quá trình dựng cây đã được thực hiện 2 lần.
![Ảnh chụp màn hình bảng hiệu suất, trong đó cho thấy một số thao tác lặp đi lặp lại và có thể được thực hiện ngay cả khi không cần thiết. Những tác vụ này có thể bị trì hoãn thực thi theo yêu cầu, thay vì được thực hiện trước thời hạn.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-11.png?authuser=9&hl=vi)
Có hai vấn đề chúng tôi phát hiện được về hình ảnh này:
- Một tác vụ không quan trọng đã cản trở hiệu suất của thời gian tải. Không phải lúc nào người dùng cũng cần kết quả. Do đó, tác vụ này không quan trọng đối với việc tải hồ sơ.
- Kết quả của những việc cần làm này không được lưu vào bộ nhớ đệm. Đó là lý do tại sao cây được tính toán hai lần, mặc dù dữ liệu không thay đổi.
Chúng tôi đã bắt đầu bằng việc trì hoãn việc tính toán cây cho đến thời điểm người dùng mở chế độ xem dạng cây theo cách thủ công. Chỉ khi đó mới đáng trả giá để tạo ra những cây này. Tổng thời gian chạy hai lần này là khoảng 3,4 giây, vì vậy việc trì hoãn đã tạo ra sự khác biệt đáng kể về thời gian tải. Chúng tôi vẫn đang xem xét việc lưu các loại tác vụ này vào bộ nhớ đệm.
Nhóm hoạt động thứ năm: tránh hệ phân cấp lệnh gọi phức tạp khi có thể
Xem xét kỹ nhóm này, rõ ràng là một chuỗi lệnh gọi cụ thể đã được gọi nhiều lần. Cùng một mô hình xuất hiện 6 lần ở những nơi khác nhau trong biểu đồ ngọn lửa, và tổng thời lượng của cửa sổ này là khoảng 2,4 giây!
![Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy 6 lệnh gọi hàm riêng biệt để tạo cùng một bản đồ thu nhỏ về dấu vết, mỗi lệnh có ngăn xếp lệnh gọi sâu.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-12.png?authuser=9&hl=vi)
Mã liên quan được gọi nhiều lần là phần xử lý dữ liệu cần kết xuất trên "bản đồ thu nhỏ" (thông tin tổng quan về hoạt động theo dòng thời gian ở đầu bảng điều khiển). Tôi không hiểu rõ lý do tại sao sự cố đó xảy ra nhiều lần, nhưng chắc chắn sự kiện không phải xảy ra đến 6 lần! Trên thực tế, kết quả của mã vẫn sẽ là hiện tại nếu không có hồ sơ nào khác được tải. Theo lý thuyết, mã chỉ nên chạy một lần.
Sau khi điều tra, chúng tôi nhận thấy mã liên quan được gọi do có nhiều phần trong quy trình tải gọi trực tiếp hoặc gián tiếp đến hàm tính toán bản đồ thu nhỏ. Nguyên nhân là do tính phức tạp của biểu đồ lệnh gọi trong chương trình đã phát triển theo thời gian, có thêm nhiều phần phụ thuộc vào mã này mà bạn vô tình thêm vào. Không có cách khắc phục nhanh cho vấn đề này. Cách giải quyết phụ thuộc vào kiến trúc của cơ sở mã được đề cập. Trong trường hợp này, chúng ta phải giảm độ phức tạp của hệ phân cấp lệnh gọi xuống một chút và thêm một dấu kiểm để ngăn quá trình thực thi mã nếu dữ liệu đầu vào không thay đổi. Sau khi triển khai việc này, chúng tôi đã xác định được tiến trình triển khai như sau:
![Ảnh chụp màn hình bảng điều khiển hiệu suất, trong đó cho thấy 6 lệnh gọi hàm riêng biệt để tạo cùng một bản đồ thu nhỏ về dấu vết đã giảm xuống chỉ còn 2 lần.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-13.png?authuser=9&hl=vi)
Lưu ý việc thực thi kết xuất bản đồ thu nhỏ xảy ra hai lần, không phải một lần. Điều này là do có hai bản đồ nhỏ được vẽ cho mỗi hồ sơ: một bản đồ cho tổng quan ở đầu bảng điều khiển và bản đồ còn lại cho trình đơn thả xuống để chọn hồ sơ đang hiển thị từ nhật ký (mỗi mục trong trình đơn này đều chứa tổng quan về hồ sơ được chọn). Tuy nhiên, cả hai đều có nội dung hoàn toàn giống nhau, do đó một trong hai có thể sử dụng lại cho nội dung còn lại.
Vì cả hai bản đồ thu nhỏ này đều được vẽ trên một canvas, nên bạn cần phải sử dụng tiện ích canvas drawImage
và sau đó chỉ chạy mã một lần để tiết kiệm thêm thời gian. Kết quả của nỗ lực này là thời lượng của nhóm đã giảm từ 2, 4 giây xuống còn 140 mili giây.
Kết luận
Sau khi áp dụng tất cả các bản sửa lỗi này (và một vài bản sửa lỗi nhỏ hơn ở đây), thay đổi của tiến trình tải hồ sơ như sau:
Trước:
![Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy dấu vết đang tải trước khi tối ưu hoá. Quá trình này mất khoảng 10 giây.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-14.png?authuser=9&hl=vi)
Sau:
![Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy dấu vết đang tải sau khi tối ưu hoá. Quá trình này hiện mất khoảng 2 giây.](https://developer.chrome.com/static/blog/perf-panel-4x-faster/image/fig-15.png?authuser=9&hl=vi)
Thời gian tải sau khi cải tiến là 2 giây, có nghĩa là cải thiện được khoảng 80% đã đạt được mà không tốn nhiều công sức, vì hầu hết những việc thực hiện đều là các bản sửa lỗi nhanh. Tất nhiên, việc xác định đúng việc cần làm ban đầu là rất quan trọng, và bảng điều khiển hiệu suất là công cụ phù hợp để làm việc này.
Một điều quan trọng cần lưu ý là những số liệu này là dành riêng cho một hồ sơ đang được dùng làm đối tượng nghiên cứu. Hồ sơ này rất thú vị với chúng tôi vì nó đặc biệt lớn. Tuy nhiên, vì quy trình xử lý là như nhau cho mọi hồ sơ, nên điểm cải tiến đáng kể đã đạt được sẽ áp dụng cho mọi hồ sơ được tải trong bảng điều khiển hiệu suất.
Cướp lại bóng
Có một số bài học rút ra từ các kết quả này về tối ưu hoá hiệu suất của ứng dụng:
1. Sử dụng công cụ phân tích tài nguyên để xác định mẫu hiệu suất trong thời gian chạy
Các công cụ phân tích tài nguyên cực kỳ hữu ích trong việc tìm hiểu những gì đang xảy ra trong ứng dụng của bạn khi ứng dụng đang chạy, đặc biệt là để xác định các cơ hội cải thiện hiệu suất. Bảng điều khiển Hiệu suất trong Công cụ của Chrome cho nhà phát triển là một lựa chọn tuyệt vời cho các ứng dụng web vì đây là công cụ lập hồ sơ web gốc trong trình duyệt và được liên tục duy trì để được cập nhật các tính năng mới nhất của nền tảng web. Ngoài ra, tốc độ còn nhanh hơn nhiều! 😉
Sử dụng các mẫu có thể dùng làm khối lượng công việc tiêu biểu để xem bạn có thể tìm được gì!
2. Tránh hệ phân cấp lệnh gọi phức tạp
Khi có thể, hãy tránh tạo biểu đồ cuộc gọi quá phức tạp. Với hệ phân cấp lệnh gọi phức tạp, bạn sẽ dễ dàng gây ra lỗi hồi quy hiệu suất và khó hiểu được lý do khiến mã của bạn chạy theo cách đó, dẫn đến việc cải thiện mã trở nên khó khăn.
3. Xác định công việc không cần thiết
Việc các cơ sở mã cũ thường chứa mã không còn cần thiết nữa. Trong trường hợp của chúng ta, mã cũ và không cần thiết đã chiếm một phần đáng kể trong tổng thời gian tải. Việc loại bỏ quả cầu đó là loại quả được treo thấp nhất.
4. Sử dụng cấu trúc dữ liệu một cách phù hợp
Sử dụng cấu trúc dữ liệu để tối ưu hoá hiệu suất, nhưng cũng hiểu rõ chi phí và lợi ích mà mỗi loại cấu trúc dữ liệu mang lại khi quyết định loại cấu trúc nên dùng. Điều này không chỉ phức tạp về không gian của chính cấu trúc dữ liệu mà còn cả độ phức tạp về thời gian của các thao tác liên quan.
5. Lưu kết quả vào bộ nhớ đệm để tránh công việc trùng lặp cho các hoạt động phức tạp hoặc lặp lại
Nếu thao tác này tốn kém chi phí thực hiện, thì tốt hơn là bạn nên lưu trữ kết quả của nó cho lần tiếp theo cần đến. Việc này cũng hợp lý nếu thao tác được thực hiện nhiều lần – ngay cả khi mỗi lần riêng lẻ không tốn nhiều chi phí.
6. Trì hoãn công việc không quan trọng
Nếu bạn không cần kết quả đầu ra của một tác vụ ngay lập tức và quá trình thực thi tác vụ đang mở rộng đường dẫn quan trọng, hãy cân nhắc trì hoãn bằng cách gọi từng phần khi đầu ra thực sự cần thiết.
7. Sử dụng thuật toán hiệu quả trên dữ liệu đầu vào lớn
Đối với dữ liệu đầu vào lớn, các thuật toán độ phức tạp về thời gian tối ưu trở nên quan trọng. Trong ví dụ này, chúng tôi đã không xem xét danh mục này, nhưng khó có thể nói quá tầm quan trọng của chúng.
8. Bật mí thêm cho bạn: đo điểm chuẩn cho quy trình của bạn
Để đảm bảo mã đang phát triển của bạn luôn nhanh chóng, bạn nên theo dõi hành vi và so sánh với tiêu chuẩn. Bằng cách này, bạn sẽ chủ động xác định sự hồi quy và cải thiện độ tin cậy tổng thể, giúp bạn thành công lâu dài.