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, đảm bảo ứng dụng tải nhanh và mang lại trải nghiệm tương tác mượt mà là yếu 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. 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 diễn ra trong 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 tài nguyên hiệu quả để phân tích và tối ưu hoá hiệu suất của ứng dụng web. Nếu ứng dụng của bạn đang chạy trong Chrome, thì ứng dụng đó sẽ cung cấp cho bạn thông tin tổng quan chi tiết về những gì trình duyệt đang làm khi ứng dụng của bạn đang được thực thi. Việc hiểu rõ 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à đ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 sử dụng bảng điều khiển Hiệu suất.
Thiết lập và tạo lại trường hợp lập hồ sơ
Gần đây, chúng tôi đã đặt mục tiêu cải thiện hiệu suất của bảng điều khiển Hiệu suất. Cụ thể, chúng tôi muốn công cụ này tải nhanh hơn các dữ liệu hiệu suất có dung lượng lớn. Ví dụ: trường hợp này xảy ra khi phân tích các quy trình phức tạp hoặc chạy trong thời gian dài hoặc 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 cách ứng dụng đang hoạt động và lý do ứng dụng hoạt động theo cách đó. Bạn có thể đạt được điều 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, bản thân DevTools là một ứng dụng web. Do đó, bạn có thể lập hồ sơ cho ứng dụng bằng bảng điều khiển Hiệu suất. Để lập hồ sơ cho chính bảng điều khiển này, bạn có thể mở DevTools, sau đó mở một thực thể DevTools khác được đính kèm vào bảng điều khiển đó. Tại Google, cách thiết lập này được gọi là DevTools-on-DevTools (Công cụ cho nhà phát triển trên Công cụ cho nhà phát triển).
Khi đã thiết lập xong, bạn phải tạo lại và ghi lại trường hợp cần phân tích. Để tránh nhầm lẫn, cửa sổ DevTools ban đầu sẽ được gọi là "phiên bản DevTools đầ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 DevTools thứ hai".
Trên phiên bản DevTools thứ hai, bảng điều khiển Hiệu suất (từ đây gọi là bảng điều khiển hiệu suất) sẽ quan sát phiên bản DevTools đầu tiên để tạo lại trường hợp này, trong đó sẽ tải một hồ sơ.
Trên thực thể DevTools thứ hai, quá trình ghi trực tiếp sẽ bắt đầu, trong khi trên thực thể thứ nhất, hồ sơ sẽ được tải từ một tệp trên ổ đĩa. Tải một tệp lớn để phân tích chính xác hiệu suất xử lý các 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 gọi là dấu vết) sẽ xuất hiện trong phiên bản DevTools thứ hai của bảng điều khiển hiệu suất đang tải một hồ sơ.
Trạng thái ban đầu: xác định cơ hội cải thiện
Sau khi tải xong, chúng ta có thể quan sát thấy những thông tin sau trên thực thể 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, hoạt động này xuất hiện trong kênh có nhãn Main (Chính). Có thể thấy rằng có 5 nhóm hoạt động lớn trong biểu đồ hình ngọn lửa. Các tác vụ này bao gồm những tác vụ mà quá trình tải mất nhiều thời gian 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, bảng hiệu suất được dùng để tập trung vào từng nhóm hoạt động này để xem có thể tìm thấy gì.
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à công sức lãng phí. Đó là một chiến thắng nhanh chóng. Việc xoá lệnh gọi hàm đó đã tiết kiệm được 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ư với nhóm hoạt động đầu tiên. buildProfileCalls
mất khoảng 0, 5 giây và không thể tránh khỏi việc thực hiện tác vụ đó.
Vì tò mò, chúng tôi đã bật tuỳ chọn Memory (Bộ nhớ) trong bảng điều khiển hiệu suất để điều tra thêm và nhận thấy hoạt động buildProfileCalls
cũng đang sử dụng nhiều bộ nhớ. Tại đây, bạn có thể thấy biểu đồ dạng đường màu xanh dương đột nhiên tăng vọt vào khoảng thời gian buildProfileCalls
chạy, cho thấy có thể xảy ra rò rỉ bộ nhớ.
Để 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 DevTools, 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ớ, loại hồ sơ "Lấy mẫu phân bổ" đã được chọn, loại hồ sơ này 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 đang tải hồ sơ CPU.
Ảnh chụp màn hình sau đây cho thấy ảnh chụp nhanh vùng nhớ khối xếp đã được thu thập.
Từ ảnh chụp nhanh vùng nhớ khối xếp này, chúng tôi nhận thấy lớp Set
đang tiêu tốn nhiều bộ nhớ. Bằng cách kiểm tra các điểm gọi, chúng tôi nhận thấy rằng chúng ta không cần thiết phải gán các thuộc tính thuộc loại Set
cho các đối tượng được tạo với số lượng lớn. Chi phí này tăng lên và tiêu tốn nhiều bộ nhớ, đến mức ứng dụng thường gặp sự cố khi nhập dữ liệu lớn.
Tập hợp hữu ích cho việc lưu trữ các mục riêng biệt và cung cấp các thao tác sử dụng tính riêng biệt của nội dung, chẳng hạn như loại bỏ trùng lặp tập dữ liệu và cung cấp các lượt 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à duy nhất từ nguồn. Do đó, ban đầu, các tập hợp là không cần thiết. Để cải thiện việc 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, một ảnh chụp nhanh vùng nhớ khối xếp khác đã được chụp và mức phân bổ bộ nhớ đã giảm. Mặc dù không đạt được mức cải thiện đáng kể về tốc độ với thay đổi này, nhưng lợi ích thứ hai là ứng dụng ít gặp sự cố hơn.
Nhóm hoạt động thứ ba: cân nhắc các đánh đổi về cấu trúc dữ liệu
Phần thứ ba khá đặ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à các lệnh đệ 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. Khi xem phần dưới cùng của phần này, rõ ràng là 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 rằng đó có thể là nút thắt cổ chai
Bên 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 nhập dữ liệu trong dữ liệu đầu vào (được gọi trong mã 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 nhập dòng thời gian. Điều này gây ra vấn đề vì số lượng mục được lưu trữ rất lớn. Map có tốc độ nhanh cho các lượt tra cứu dựa trên khoá, nhưng lợi thế này không phải là miễn phí. Khi một 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 tạo lại hàm băm. Chi phí này trở nên đáng kể khi một lượng lớn mục được thêm vào bản đồ 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 chúng tôi thêm một mục trong bản đồ cho mỗi mục nhập trong biểu đồ hình ngọn lửa. Mức cải thiện là đáng kể, xác nhận rằng nút thắt cổ chai thực sự liên quan đến chi phí phát sinh khi thêm tất cả dữ liệu vào bản đồ. Thời gian nhóm hoạt động mất đã giảm từ khoảng 1,4 giây xuống còn khoảng 200 mili giây.
Trước:
Sau:
Nhóm hoạt động thứ tư: trì hoãn công việc không quan trọng và dữ liệu bộ nhớ đệm để tránh công việc trùng lặp
Khi phóng to cửa sổ này, bạn có thể thấy 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ã đang tạo cây (ví dụ: có tên như refreshTree
hoặc buildChildren
). Trên thực tế, mã liên quan là mã tạo thành phần hiển thị 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 xuất hiện ngay sau khi tải. Thay vào đó, người dùng cần chọn chế độ xem cây (thẻ "Dưới lên", "Cây lệnh gọi" và "Nhật ký sự kiện" trong ngăn) để các cây hiển thị. Hơn nữa, như bạn có thể thấy trong ảnh chụp màn hình, quy trình tạo cây đã được thực thi hai lần.
Chúng tôi đã xác định được hai vấn đề với bức ảnh này:
- Một tác vụ không quan trọng đang cản trở hiệu suất của thời gian tải. Người dùng không phải lúc nào cũng cần kết quả của hàm này. 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 các tác vụ này không được lưu vào bộ nhớ đệm. Đó là lý do tại sao các cây được tính toán hai lần, mặc dù dữ liệu không thay đổi.
Chúng ta bắt đầu bằng cách trì hoãn việc tính toán cây cho đến khi người dùng mở chế độ xem cây theo cách thủ công. Chỉ khi đó, bạn mới nên trả giá để tạo 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 cũng đ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ể
Khi xem xét kỹ nhóm này, rõ ràng là một chuỗi lệnh gọi cụ thể đang được gọi lại nhiều lần. Mẫu tương tự xuất hiện 6 lần ở nhiều vị trí trong biểu đồ hình 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!
Mã liên quan được gọi nhiều lần là phần xử lý dữ liệu sẽ được kết xuất trên "bản đồ thu nhỏ" (thông tin tổng quan về hoạt động trên dòng thời gian ở đầu bảng điều khiển). Không rõ lý do tại sao điều này xảy ra nhiều lần, nhưng chắc chắn là không phải 6 lần! Trên thực tế, kết quả của mã sẽ vẫn giữ nguyên nếu không có hồ sơ nào khác được tải. Về 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 là kết quả của nhiều phần trong quy trình tải trực tiếp hoặc gián tiếp gọi hàm tính bản đồ thu nhỏ. Điều này là do độ phức tạp của biểu đồ lệnh gọi của chương trình đã phát triển theo thời gian và nhiều phần phụ thuộc hơn đã được thêm vào mã này mà không hề hay biết. Không có cách khắc phục nhanh cho vấn đề này. Cách giải quyết vấn đề này phụ thuộc vào cấu trúc của cơ sở mã có liên quan. Trong trường hợp của chúng ta, chúng ta phải giảm bớt độ phức tạp của hệ phân cấp lệnh gọi và thêm một bước kiểm tra để ngăn việc thực thi mã nếu dữ liệu đầu vào không thay đổi. Sau khi triển khai, chúng tôi có được thông tin tổng quan về tiến trình như sau:
Xin lưu ý rằng quá trình thực thi kết xuất bản đồ thu nhỏ xảy ra hai lần, chứ không phải một lần. Điều này là do có hai bản đồ thu nhỏ được vẽ cho mỗi hồ sơ: một bản đồ cho thông tin tổng quan ở đầu bảng điều khiển và một bản đồ cho trình đơn thả xuống để chọn hồ sơ hiện hiển thị trong nhật ký (mỗi mục trong trình đơn này chứa thông tin tổng quan về hồ sơ mà nó chọn). Tuy nhiên, hai tệp này có nội dung giống hệt nhau, vì vậy, bạn có thể sử dụng lại một tệp cho tệp còn lại.
Vì cả hai bản đồ thu nhỏ này đều là hình ảnh được vẽ trên canvas, nên bạn chỉ cần 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. Nhờ đó, 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ỏ khác), tiến trình tải hồ sơ sẽ thay đổi như sau:
Trước:
Sau:
Thời gian tải sau khi cải thiện là 2 giây, tức là tăng khoảng 80% mà không tốn nhiều công sức, vì hầu hết những gì đã làm đề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 cho việc này.
Ngoài ra, điều quan trọng cần làm nổi bật là những con số này dành riêng cho một hồ sơ được dùng làm đối tượng nghiên cứu. Hồ sơ này rất thú vị đối với chúng tôi vì có kích thước đặc biệt lớn. Tuy nhiên, vì quy trình xử lý giống nhau đối với mọi hồ sơ, nên sự cải thiệ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ừ những kết quả này về việc tối ưu hoá hiệu suất của ứng dụng:
1. Sử dụng các công cụ phân tích tài nguyên để xác định các 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 để hiểu những gì đang diễn ra trong ứng dụng của bạn trong khi ứng dụng đang chạy, đặc biệt là để xác định những 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ụ phân tích tài nguyên web gốc trong trình duyệt và được duy trì tích cực để luôn cập nhật các tính năng mới nhất của nền tảng web. Ngoài ra, giờ đây, công cụ này nhanh hơn đáng kể! 😉
Hãy sử dụng các mẫu có thể dùng làm khối lượng công việc đại diện và xem bạn có thể tìm thấy gì!
2. Tránh hệ phân cấp lệnh gọi phức tạp
Khi có thể, hãy tránh làm cho biểu đồ lệnh gọi của bạn quá phức tạp. Với hệ phân cấp lệnh gọi phức tạp, bạn dễ dàng gặp phải tình trạng hồi quy hiệu suất và khó hiểu lý do mã của mình đang chạy như vậy, khiến bạn khó có thể cải thiện.
3. Xác định công việc không cần thiết
Thường thì các cơ sở mã cũ sẽ chứa mã không còn cần thiết. 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 xoá lớp phủ này là việc dễ dàng 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 được chi phí và sự đánh đổi mà mỗi loại cấu trúc dữ liệu mang lại khi quyết định sử dụng loại nào. Đây không chỉ là độ phức tạp về không gian của chính cấu trúc dữ liệu, mà còn là độ phức tạp về thời gian của các phép toán có thể áp dụng.
5. Lưu kết quả vào bộ nhớ đệm để tránh lặp lại công việc cho các thao tác phức tạp hoặc lặp lại
Nếu tốn kém để thực thi thao tác, bạn nên lưu trữ kết quả của thao tác đó cho lần tiếp theo cần đến. Bạn cũng nên làm việc này nếu thao tác được thực hiện nhiều lần, ngay cả khi mỗi lần thực hiện không tốn kém nhiều.
6. Hoãn công việc không quan trọng
Nếu không cần kết quả của một tác vụ ngay lập tức và quá trình thực thi tác vụ đang kéo dài đường dẫn quan trọng, hãy cân nhắc trì hoãn tác vụ đó bằng cách gọi tác vụ đó một cách lười biếng khi thực sự cần kết quả của tác vụ đó.
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 có độ phức tạp về thời gian tối ưu trở nên rất quan trọng. Chúng ta không xem xét danh mục này trong ví dụ này, nhưng khó có thể đánh giá quá mức tầm quan trọng của các danh mục này.
8. Phần thưởng: đo điểm chuẩn cho quy trình
Để đảm bảo mã đang phát triển của bạn vẫn nhanh, bạn nên theo dõi hành vi và so sánh hành vi đó với các tiêu chuẩn. Bằng cách này, bạn có thể chủ động xác định các hồi quy và cải thiện độ tin cậy tổng thể, giúp bạn đạt được thành công lâu dài.