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 cũng như mang đến các hoạt động tương tác mượt mà là điều 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 một ứng dụng bằng cách sử dụng các công cụ lập hồ sơ để xem những gì đang diễn ra bên 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ụ lập hồ sơ tuyệt vời để 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, thì công cụ này sẽ cung cấp cho bạn thông tin tổng quan chi tiết bằng hình ảnh về những gì trình duyệt đang làm khi ứng dụng của bạn đang được thực thi. Khi hiểu rõ hoạt động này, bạn có thể 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 sẽ 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ơ của chúng tôi
Gần đây, chúng tôi đã đặt ra 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 một lượng lớn dữ liệu hiệu suất nhanh hơn. Ví dụ: trường hợp này xảy ra khi bạn lập hồ sơ các quy trình phức tạp hoặc chạy trong thời gian dài hoặc ghi lại dữ liệu có độ chi tiết cao. Để đạt được điều này, trước tiên, chúng tôi cần hiểu cách ứng dụng hoạt động và lý do ứng dụng hoạt động theo cách đó. Chúng tôi đã đạt được điều này bằng cách sử dụng một công cụ lập hồ sơ.
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ơ cho thành phần này 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ở 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ào bảng điều khiển đó. Tại Google, chế độ thiết lập này được gọi là Công cụ cho nhà phát triển trên Công cụ cho nhà phát triển.
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 thứ nhất" và cửa sổ đang kiểm tra phiên bản đầu tiên sẽ được gọi là "phiên bản Công cụ cho nhà phát triển thứ hai".

Trên phiên bản Công cụ cho nhà phát triển thứ hai, bảng điều khiển Hiệu suất (từ đây trở đi sẽ được gọi là bảng điều khiển hiệu suất) sẽ quan sát phiên bản Công cụ cho nhà phát triển đầu tiên để tạo lại tình huống tải một hồ sơ.
Trên phiên bản DevTools thứ hai, một bản ghi trực tiếp sẽ bắt đầu, trong khi trên phiên bản đầu tiên, một hồ sơ sẽ được tải từ một tệp trên đĩa. Một tệp lớn được tải để lập hồ sơ chính xác về 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 lập hồ sơ hiệu suất (thường được gọi là dấu vết) sẽ xuất hiện trong phiên bản Công cụ cho nhà phát triển thứ hai của bảng điều khiển perf đang tải một hồ sơ.
Trạng thái ban đầu: xác định các cơ hội cải thiện
Sau khi quá trình tải hoàn tất, bạn sẽ thấy thông tin 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, xuất hiện trong bản ghi có nhãn Main. Bạn có thể thấy có 5 nhóm hoạt động lớn trong biểu đồ ngọn lửa. Đây là 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 đây, bảng điều khiển hiệu suất được dùng để tập trung vào từng nhóm hoạt động này nhằm xem những gì có thể tìm thấy.

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 vô ích. Đó là một chiến thắng nhanh chóng. Việ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ư nhóm hoạt động đầu tiên. buildProfileCalls
mất khoảng 0, 5 giây và đó là việc không thể tránh khỏi.

Vì tò mò, chúng tôi đã bật lựa chọn Memory (Bộ nhớ) trong bảng điều khiển hiệu suất để tìm hiểu 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 tăng lên vào khoảng thời gian chạy buildProfileCalls
, cho thấy có thể xảy ra tình trạng 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 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ớ, loại lập hồ sơ "Lấy mẫu phân bổ" đã được chọn, ghi lại ảnh chụp nhanh heap để bảng điều khiển perf tải hồ sơ CPU.

Ảnh chụp màn hình sau đây cho thấy ảnh chụp nhanh heap đã được thu thập.

Từ ảnh chụp nhanh vùng nhớ khối xếp này, người ta nhận thấy rằng lớp Set
đang tiêu thụ rất 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 tôi đang gán không cần thiết 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 có dữ liệu đầu vào lớn.
Tập hợp rất hữu ích để 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ỏ dữ liệu trùng lặp và cung cấp các hoạt động tra cứu hiệu quả hơn. Tuy nhiên, những tính năng đó là không cần thiết vì dữ liệu được lưu trữ chắc chắn là duy nhất so với nguồn. Do đó, bạn không cần phải tạo tập hợp ngay từ đầu. Để 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 bản tổng quan nhanh khác về heap đã được chụp và mức phân bổ bộ nhớ giảm xuống. Mặc dù không đạt được tốc độ cải thiện đáng kể 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 điểm đánh đổi về cấu trúc dữ liệu
Phần thứ ba có đặc điểm riêng: bạn có thể thấy trong biểu đồ hình ngọn lửa rằng 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 đệ 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 xét phần cuối 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 bằng thời lượng của một hàm: appendEventAtLevel
, cho thấy rằng đây có thể là một điểm tắc nghẽ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 đầu vào (được gọi là "sự kiện" trong mã), 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 trên dòng thời gian. Điều này gây ra vấn đề vì số lượng mặt hàng được lưu trữ rất lớn. Tập hợp map có tốc độ tìm kiếm dựa trên khoá nhanh, nhưng lợi thế này không phải là miễn phí. Khi bản đồ lớn hơn, việc thêm dữ liệu vào bản đồ có thể trở nên tốn kém, chẳng hạn như do việc băm lại. Chi phí này sẽ trở nên đáng chú ý khi bạn thêm nhiều mục 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 vào bản đồ cho mỗi mục trong biểu đồ ngọn lửa. Sự cải thiện này là đáng kể, xác nhận rằng điểm tắc nghẽn 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 thực hiện 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:

Sau:

Nhóm hoạt động thứ tư: hoãn công việc không quan trọng và lưu dữ liệu vào bộ nhớ đệm để tránh trùng lặp công việc
Khi phóng to cửa sổ này, bạn có thể thấy có 2 khối lệnh gọi hàm gần giống nhau. Bằng cách xem tên của các hàm được gọi, bạn có thể suy ra 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 ra các khung 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à những khung hiển thị 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 ("Bottom-up", "Call Tree" và "Event Log" 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.

Chúng tôi nhận thấy có 2 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. Không phải lúc nào người dùng cũng cần đầu ra của nó. 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 tác vụ này không được lưu vào bộ nhớ đệm. Đó là lý do khiến các 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 cách hoãn 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 đó, việc trả giá để tạo ra những cây này mới có ý nghĩa. Tổng thời gian chạy hai lần này là khoảng 3,4 giây, vì vậy việc hoãn lại đã 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 vào bộ nhớ đệm các loại tác vụ này.
Nhóm hoạt động thứ năm: tránh các hệ thống 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 nhiều lần. Cùng một mẫu xuất hiện 6 lần ở các vị trí 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!

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 khiến việc này xảy ra nhiều lần, nhưng chắc chắn là không cần thiết phải xảy ra đến 6 lần! Trên thực tế, đầu ra của mã sẽ vẫn là đầu ra hiện tại nếu không có hồ sơ nào khác được tải. Về lý thuyết, mã này chỉ 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 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 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 vào mã này đã được thêm vào mà không 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 đề cập. Trong trường hợp này, chúng ta phải giảm bớt độ phức tạp của hệ thống 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 cái nhìn tổng quan về dòng thời gian như sau:

Xin lưu ý rằng quá trình thực thi kết xuất bản đồ thu nhỏ diễn ra 2 lần chứ không phải 1 lần. Điều này là do có hai bản đồ thu nhỏ được vẽ cho mỗi hồ sơ: một cho thông tin tổng quan ở trên cùng của bảng điều khiển và một cho trình đơn thả xuống chọn hồ sơ hiện đang hiển thị trong nhật ký (mỗi mục trong trình đơn này đều chứa thông tin tổng quan về hồ sơ mà mục đó chọn). Tuy nhiên, hai thành phần 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 thành phần cho thành phần còn lại.
Vì cả hai bản đồ thu nhỏ này đều là hình ảnh được vẽ trên một canvas, nên bạn chỉ cần dùng drawImage
tiện ích canvas, sau đó 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 số bản sửa lỗi nhỏ khác), thay đổi về dòng thời gian tải hồ sơ sẽ như sau:
Trước:

Sau:

Thời gian tải sau khi cải thiện là 2 giây, tức là mức cải thiện khoảng 80% đã đạt được với tương đối ít công sức, vì hầu hết những gì đã 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 điều gì 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.
Bạn cũng cần lưu ý rằng những con số này chỉ áp dụ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ị đố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ý là như nhau đối với mọi hồ sơ, nên mức 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ụ lập hồ sơ để xác định các mẫu hiệu suất thời gian chạy
Các công cụ phân tích hiệu suất rất hữu ích để tìm hiểu những gì đang diễn ra trong ứng dụng của bạn 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ụ cho nhà phát triển của Chrome 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 duy trì liên tụ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, tính năng này hoạt động nhanh hơn đáng kể! 😉
Hãy sử dụng các mẫu có thể dùng làm tải trọng đại diện và xem bạn có thể tìm thấy những 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 đồ cuộc gọi của bạn quá phức tạp. Với hệ thống phân cấp lệnh gọi phức tạp, bạn có thể dễ dàng gặp phải tình trạng giảm hiệu suất và khó hiểu lý do mã của bạn chạy theo cách đó, khiến bạn khó cải thiện hiệu suất.
3. Xác định những công việc không cần thiết
Các cơ sở mã cũ thường chứa mã không còn cần thiết. Trong trường hợp của chúng tôi, 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ỏ nó là điều 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 cần hiểu rõ chi phí và những điểm đánh đổi mà mỗi loại cấu trúc dữ liệu mang lại khi quyết định sử dụng cấu trúc dữ liệu 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 thao tác có thể áp dụng.
5. Lưu kết quả vào bộ nhớ đệm để tránh trùng lặp công việc cho các thao tác phức tạp hoặc lặp đi lặp lại
Nếu thao tác này tốn nhiều chi phí để thực thi, thì bạn nên lưu trữ kết quả của thao tác này cho lần tiếp theo cần đến. Bạn cũng nên làm như vậy nếu thao tác được thực hiện nhiều lần, ngay cả khi mỗi lần không tốn nhiều thời gian.
6. Hoãn công việc không quan trọng
Nếu không cần ngay đầu ra của một tác vụ 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 một cách gián tiếp khi đầu ra của tác vụ thực sự cần thiết.
7. Sử dụng các thuật toán hiệu quả trên dữ liệu đầu vào lớn
Đối với các đầu vào lớn, các thuật toán có độ phức tạp thời gian tối ưu trở nên rất quan trọng. Chúng tôi không xem xét danh mục này trong ví dụ này, nhưng tầm quan trọng của danh mục này là không thể phủ nhận.
8. Phần thưởng: đo điểm chuẩn cho các kênh của bạn
Để đảm bảo mã của bạn luôn hoạt động nhanh chóng, bạn nên theo dõi hành vi và so sánh với các tiêu chuẩn. Bằng cách này, bạn chủ động xác định các trường hợp 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.