Vào ngày 2 tháng 2 năm 2017 vừa qua, Ann Taylor đã viết bài viết 12 điều chúng ta cần thấy từ các WordPress Page Builder vào năm 2017 + Ai đã làm đúng trên CodeInWP. Chúng tôi đã liên tục cải tiến Page Builder Sandwich trong hơn một năm qua: chúng tôi nghiên cứu, lắng nghe khách hàng, vì vậy chúng tôi đã nghĩ rằng Page Builder Sandwich sẽ lọt vào bài viết “Ai đã làm đúng” của CodeInWP. Tuy nhiên, chúng tôi đã không có tên trong đó, vì vấn đề hiệu năng. 
Vì vậy, chúng tôi đã hỏi Ann về điều đó và cô ấy trả lời:
…Tôi đã dùng thử phiên bản miễn phí một chút. Nó rất hứa hẹn: nhiều mô-đun, giao diện người dùng khá trực quan, trải nghiệm WYSIWYG tuyệt vời, mọi thứ đều được kéo thả dễ dàng. Tuy nhiên, khi tôi có khoảng 10 mô-đun trên trang, việc tiếp tục làm việc trở nên khó khăn do tốc độ chỉnh sửa chậm…
Vì vậy theo cô ấy, Page Builder Sandwich gặp sự cố về hiệu năng. Và vì chúng tôi luôn lắng nghe phản hồi, chúng tôi đã tiến hành kiểm tra.
Kiểm tra hiệu năng
Chúng tôi cần một cách để kiểm tra hiệu năng của PBS; vì PBS chủ yếu chạy trên trình duyệt thông qua Javascript, nên trình phân tích Timeline của Chrome là công cụ hoàn hảo cho công việc này. Trình phân tích timeline có thể xác định và đo lường các tiến trình đang diễn ra trên một trang web trong thời gian chạy, điều này hoàn hảo để tìm ra các điểm nghẽn tốc độ.
Chúng tôi nghĩ rằng trước tiên nên kiểm tra các bước đơn giản nhất – rê chuột qua nhiều phần tử và hàng. Đơn giản phải không? Chúng tôi tự tin rằng mình sẽ không gặp phải vấn đề về hiệu năng với một việc đơn giản như thế này. Đáng tiếc là chúng tôi đã nhầm.
Sau khi nhấn nút ghi (record), chúng tôi rê chuột khoảng 2 giây trên màn hình, rồi nhấn nút dừng. Dưới đây là biểu đồ thời gian kết quả từ trình phân tích:
Và hãy nhìn xem. Nó chứa đầy các điểm nghẽn hiệu năng!
Có rất nhiều thứ đang diễn ra trong biểu đồ, nhưng vấn đề rõ ràng nhất là có rất nhiều vùng màu tím trong đó (điều không nên xảy ra). Hơn nữa, các vùng màu tím này nhìn thấy rất rộng và dường như đều chạm đỉnh của biểu đồ. Bạn cũng sẽ nhận thấy rằng biểu đồ luôn đầy và các vùng màu tím hầu như không bao giờ giảm xuống mức 0. Có vẻ như trình duyệt luôn bận rộn làm một việc gì đó và gần như không bao giờ được nghỉ ngơi.
Nhưng tất cả các vùng màu tím đó thực sự có ý nghĩa gì?
Trình phân tích đã chỉ ra cho chúng tôi biết rằng chúng được gọi là reflow. Dưới đây là định nghĩa về reflow từ trang dành cho nhà phát triển của Google:
Reflow là tên của tiến trình trình duyệt web dùng để tính toán lại vị trí và hình học của các phần tử trong tài liệu.. reflow là một thao tác chặn người dùng (user-blocking) trong trình duyệt.
Điều này có nghĩa là việc rê chuột trong PBS kích hoạt một loạt reflow. Điều tồi tệ hơn là mỗi lần reflow lại tạm dừng trình duyệt trong chốc lát để hoàn thành các phép tính toán. Càng nhiều reflow đồng nghĩa với việc trình duyệt tạm dừng càng nhiều và trải nghiệm càng trở nên giật lag (janky). Nói một cách đơn giản hơn, nhiều vùng màu tím = tệ.
Hóa ra Ann đã đúng: chúng tôi thực sự có một điểm nghẽn hiệu năng khổng lồ, vì vậy đội ngũ của chúng tôi đã ngay lập tức bắt tay vào việc để khắc phục nó ngay.
Khắc phục sự cố Reflow / Giật lag của chúng tôi
Trước khi làm bất cứ điều gì khác, chúng tôi cần nghiên cứu để tìm ra nguyên nhân chính xác gây ra hiện tượng reflow này ngay từ đầu. Chúng tôi phát hiện ra rằng rõ ràng, rất nhiều thao tác phổ biến trong Javascript có thể khiến trình duyệt thực hiện reflow.
Hầu như bất kỳ hàm nào bạn dùng để lấy chiều rộng, chiều cao hoặc vị trí của một phần tử DOM đều có thể kích hoạt reflow. Paul Irish đã viết một danh sách đầy đủ về tất cả các thuộc tính và hàm trong Javascript có thể gây ra điều này. Tin xấu là Page Builder Sandwich sử dụng rất nhiều thuộc tính và hàm này, tất cả đều gây ra reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- và còn nhiều nữa…
Cách khắc phục #1: Lưu vào bộ nhớ đệm các tiến trình Reflow tốn kém
Giải pháp rõ ràng nhất là KHÔNG sử dụng các thuộc tính và hàm đó. Tuy nhiên, PBS phụ thuộc rất nhiều vào chúng và chúng tôi không thể thiếu chúng.
Giải pháp còn lại cho chúng tôi là cắt giảm tối đa các lệnh gọi tới các hàm đó. Điều khó khăn ở đây là chúng tôi đã sử dụng các thuộc tính và hàm đó rất nhiều lần ở các phần khác nhau của PBS, và phần lớn thời gian, chúng tôi sử dụng chúng trên cùng một phần tử DOM nhiều lần. Để khắc phục điều này, chúng tôi đã tạo cơ chế bộ nhớ đệm (caching) riêng cho từng thuộc tính/hàm có vấn đề.
Ví dụ: hàm getBoundingClientRect cung cấp cho chúng tôi các kích thước pixel trên cùng, bên trái, chiều rộng, chiều cao, bên phải và dưới cùng hiện tại của một phần tử DOM. Chúng tôi luôn sử dụng hàm này để tính toán vị trí vẽ đường viền phần tử trong PBS. Dưới đây là hàm lưu vào bộ nhớ đệm mà chúng tôi đã tạo cho mục đích này:
// Cache: Các client rect của chúng tôi được lưu ở đây, chúng ta có thể dùng để xóa chúng sau.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Kiểm tra xem chúng ta đã lấy client rect trước đó chưa.
if ( ! element._boundingClientRect ) {
// Nếu chưa, hãy lấy rồi lưu trữ để sử dụng sau này.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Thay vì gọi getBoundingClientRect , giờ đây chúng tôi sử dụng pbsGetBoundingClientRect . Giờ đây, mỗi khi client rect được truy vấn, chúng tôi lưu giá trị đó để tham chiếu sau này, nhờ đó hạn chế số lượng kích hoạt reflow.
Chúng tôi cũng cần một cách để xóa bộ nhớ đệm khi các client rect đã lưu không còn hợp lệ. Giá trị của client rect được lưu trữ sẽ thay đổi khi người dùng cuộn trang hoặc thêm/thay đổi/xóa các phần tử trên màn hình. Vì vậy, chúng tôi đã tạo một hàm xóa bộ nhớ đệm mà chúng tôi có thể gọi bất cứ khi nào cần các giá trị client rect mới:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Chúng tôi cũng đã áp dụng cơ chế bộ nhớ đệm tương tự này cho các lệnh gọi hàm tốn kém khác mà chúng tôi thường dùng.
Cách khắc phục #2: Gom nhóm các thao tác Đọc & Ghi bằng FastDom
Việc triển khai cách khắc phục #1 đã tăng hiệu năng của PBS, nhưng vẫn chưa đủ.
Tìm kiếm thêm trên Google một chút, chúng tôi tình cờ đọc được một bài viết của Wilson Page về Ngăn chặn ‘layout thrashing’. Wilson chia sẻ:
Hiện tượng Layout Thrashing (reflow) xảy ra khi JavaScript liên tục ghi rồi đọc từ DOM nhiều lần, gây ra hiện tượng reflow tài liệu.
Rõ ràng, chỉ hạn chế các lệnh gọi đến các thuộc tính và hàm kích hoạt reflow cụ thể là chưa đủ. Khi bạn thực hiện nhiều thao tác “đọc” và “ghi” liên tiếp trên DOM, nó cũng kích hoạt reflow trình duyệt. Dưới đây là một tình huống điển hình, trích từ trang của Wilson:
// Đọc
var h1 = element1.clientHeight;
// Ghi (làm mất hiệu lực layout)
element1.style.height = (h1 * 2) + 'px';
// Đọc (kích hoạt reflow)
var h2 = element2.clientHeight;
// Ghi (làm mất hiệu lực layout)
element2.style.height = (h2 * 2) + 'px';
// Đọc (kích hoạt reflow)
var h3 = element3.clientHeight;
// Ghi (làm mất hiệu lực layout)
element3.style.height = (h3 * 2) + 'px';
// Tài liệu reflow ở cuối frame
Ví dụ trên kích hoạt 3 lần reflow vì kích thước của toàn bộ trang bị mất hiệu lực sau một lần ghi. Cách khắc phục được đề xuất cho việc này là sắp xếp lại mã để gom nhóm tất cả các lần đọc và ghi lại với nhau để toàn bộ quá trình chỉ kích hoạt 1 lần reflow trình duyệt:
// Đọc
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Ghi (làm mất hiệu lực layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Tài liệu reflow ở cuối frame
Đoạn mã trên làm cho việc khắc phục sự cố trông có vẻ dễ dàng. Nhưng trên thực tế, trong PBS, việc đọc và ghi những thứ kích hoạt reflow diễn ra ở rất nhiều nơi khác nhau vào nhiều thời điểm khác nhau trong vòng đời của PBS. Việc tổ chức lại hàng nghìn dòng mã Javascript là không khả thi.
May mắn cho chúng tôi, Wilson cũng đã cung cấp một giải pháp trong bài viết của mình. Anh ấy đã tạo một script tiện ích nhỏ để giúp gom nhóm các lần đọc và ghi đó lại với nhau, có tên là FastDom.
Dưới đây là một số mã từ Page Builder Sandwich sử dụng FastDom và cũng bao gồm Cách khắc phục #1 của chúng tôi ở trên:
// Gom nhóm các lần đọc từ các luồng khác.
fastdom.measure( function() {
// Sử dụng client rect đã lưu trong cache của chúng tôi.
var rect = window.pbsGetBoundingClientRect( elem );
// Gom nhóm các lần ghi từ các luồng khác.
fastdom.mutate( function() {
elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
elem.style.height = rect.height + 'px';
elem.style.left = rect.left + 'px';
elem.style.width = rect.width + 'px';
}.bind( this ) );
}.bind( this ) );
Những gì FastDom làm là gom nhóm tất cả các lệnh gọi fastdom.measure và tất cả các lệnh gọi fastdom.mutate lại và thực thi chúng bất đồng bộ, gom nhóm hiệu quả tất cả các lần đọc và ghi lại với nhau để giảm bớt tất cả các lần reflow trong trình duyệt.
Cách khắc phục #3: Việc truy cập các đối tượng Style rất tốn kém
Chúng tôi cũng phát hiện ra với sự trợ giúp của jsPerf rằng một số hàm lấy kiểu dáng (style) chạy nhanh hơn những hàm khác.
Trong PBS, chúng tôi thường cần lấy giá trị hiện tại của một style cụ thể của một phần tử. Ví dụ: chúng tôi thường sử dụng cách này để lấy lề trên (top margin):
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Nhưng chúng tôi phát hiện ra rằng làm như thế này nhanh hơn nhiều:
var marginTop = elem.style.marginTop;
Tuy nhiên, elem.style.marginTop và getComputedStyle rất khác nhau. Cách thứ nhất chỉ cung cấp cho chúng tôi kiểu dáng nội tuyến (inline style) của phần tử và không tính đến bất kỳ stylesheet nào; còn cách thứ hai tính đến cả kiểu dáng nội tuyến và stylesheet.
Hơn nữa, ngay cả khi kết quả của getComputedStyle được lưu vào bộ nhớ đệm, nó vẫn rất chậm. Hóa ra, việc truy cập chính đối tượng style mới là tiến trình chậm chạp.
May mắn cho chúng tôi, PBS thêm các kiểu dáng nội tuyến vào các phần tử và vì các kiểu dáng nội tuyến được ưu tiên (hầu hết thời gian) hơn các stylesheet, nên sẽ có những trường hợp việc lấy giá trị kiểu dáng nội tuyến là đủ. Vì vậy, nếu chúng tôi kết hợp hai phương pháp trên, chúng tôi có thể đưa ra một đoạn mã chạy nhanh hơn nhiều:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Kết quả cải tiến mã nguồn
3 cách khắc phục ở trên của chúng tôi đã tốn rất nhiều thời gian nghiên cứu, thử nghiệm và triển khai thực tế (và làm trì hoãn tính năng Fonts phát hành), nhưng cuối cùng tất cả đều xứng đáng.
Quay trở lại thử nghiệm ban đầu của chúng tôi là di chuyển chuột trên màn hình trong 2 giây. Đây là biểu đồ thời gian cũ đầy xấu xí/giật lag:
Và đây là diện mạo biểu đồ thời gian mới của chúng tôi sau các cải tiến hiệu năng:
Như bạn có thể thấy, có ít phần màu tím hơn trên biểu đồ. Và có sự khác biệt rõ rệt về số lượng tiến trình được trình duyệt thực hiện. Các khối hình chữ nhật ở phía dưới cũng mỏng hơn, có nghĩa là các tiến trình đang được chạy nhanh hơn trước.
Nếu bạn so sánh số lượng vùng màu tím cao/rộng trong biểu đồ cũ và mới, chúng ta đang thấy mức tăng hiệu năng ấn tượng lên tới 200%! Chúng tôi cũng nhận thấy hiệu năng tăng lên trên toàn bộ hệ thống. Không tồi chút nào!
Đúc kết & Bài học kinh nghiệm
Thành thật mà nói, chúng tôi đã không nghĩ PBS gặp vấn đề về hiệu năng trước khi Ann đề cập đến. Chủ yếu là vì chúng tôi đang sử dụng các máy chuyên dùng cho phát triển và các điểm nghẽn hiệu năng không rõ ràng như vậy. Nhìn lại, việc kiểm tra hiệu năng đáng lẽ phải là một trong những ưu tiên hàng đầu. Nhưng ít nhất bây giờ, kể từ phiên bản 4.3, nó đã được giải quyết. 
Đối với những ai muốn cải thiện hiệu năng plugin WordPress của riêng mình, đặc biệt nếu bạn cũng đang sử dụng nhiều Javascript, đây là những bài học tôi rút ra từ trải nghiệm này:
- Giới hạn/lưu vào bộ nhớ đệm các lệnh gọi tới các thuộc tính/hàm kích hoạt reflow. Sử dụng tài liệu này làm hướng dẫn: Những gì buộc phải layout/reflow. Danh sách đầy đủ.
- Gom nhóm các thao tác “đọc” và “ghi” vào các style DOM. Sử dụng FastDom để làm điều này giúp bạn.
- Sử dụng trình phân tích Timeline của Chrome và đảm bảo bạn có càng ít reflow (các phần màu tím) càng tốt.