Bảng điều khiển Hiệu suất nhanh hơn 400% nhờ tính năng nhận biết

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

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, ứ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. 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 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 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ụ: trong trường hợp này, khi phân tích tài nguyên cho 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 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. Để 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 khác của Công cụ cho nhà phát triển đượ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 (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".

Ảnh chụp màn hình của một thực thể DevTools đang kiểm tra các phần tử trong chính DevTools.
Công cụ cho nhà phát triển trên Công cụ cho nhà phát triển: kiểm tra Công cụ cho nhà phát triển bằng Công cụ cho nhà phát triển.

Trên thực thể 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 thực thể 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ý 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 có 5 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, 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 chụp màn hình bảng điều khiển hiệu suất trong DevTools đang kiểm tra quá trình tải dấu vết hiệu suất trong bảng điều khiển hiệu suất của một phiên bản DevTools khác. Hồ sơ sẽ mất khoảng 10 giây để tải. Thời gian này chủ yếu được chia thành 5 nhóm hoạt động chính.

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ư 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ể bảng điều khiển hiệu suất khác. Một tác vụ liên kết với hàm buildProfileCalls mất khoảng 0,5 giây.

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ớ.

Ả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 sử dụng bộ nhớ của bảng điều khiển hiệu suất. Trình kiểm tra cho biết hàm buildProfileCalls chịu trách nhiệm về việc 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ớ, kiểu phân tích "Phân bổ lấy mẫu" đã được chọn. Loại hồ sơ này đã ghi lại ảnh chụp nhanh của 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ớ. Tuỳ chọn "lấy mẫu phân bổ" được đánh dấu bằng hộp màu đỏ và cho biết tuỳ chọn này phù hợp nhất để phân tích tài nguyên bộ nhớ JavaScript.

Ả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.

Ảnh chụp màn hình của trình phân tích bộ nhớ, trong đó đã chọn một thao tác dựa trên Tập hợp cần nhiều bộ nhớ.

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 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à dữ liệu riêng biệ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à chúng tôi nhận thấy 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 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 trước đây tốn nhiều bộ nhớ đã được thay đổi để sử dụng một mảng thuần tuý, giúp giảm đáng kể chi phí bộ nhớ.

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

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. Maps nhanh chóng cho cá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í. 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 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 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. 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 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:

Ả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 để hàm chạy là 1.372,51 mili giây.

Sau:

Ảnh chụp màn hình bảng hiệu suất sau khi tối ưu hoá hàm appendEventAtLevel. Tổng thời gian để hàm chạy là 207,2 mili giây.

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 dạng 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 xuất hiện. 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.

Ảnh chụp màn hình của bảng điều khiển hiệu suất cho thấy một số tác vụ lặp lại được thực thi ngay cả khi không cần thiết. Bạn có thể trì hoãn các tác vụ này để thực thi theo yêu cầu thay vì trước thời hạn.

Chúng tôi đã xác định được hai vấn đề với bức ảnh này:

  1. 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ơ.
  2. 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 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!

Ả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.

Mã liên quan đang được gọi nhiều lần là phần xử lý dữ liệu cần hiển thị 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). 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. 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ỏ. Đ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:

Ảnh chụp màn hình của 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 đồ theo dõi thu nhỏ chỉ còn 2 lần.

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ờ nỗ lực này, 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:

Ả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.

Sau:

Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy quá trình tải dấu vết sau khi tối ưu hoá. Quá trình này hiện mất khoảng hai giây.

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 để làm 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ừ 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á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ụ 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, 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 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 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 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 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.