Đo điểm chuẩn hiệu suất của @property CSS

Ngày xuất bản: ngày 2 tháng 10 năm 2024

Khi bắt đầu sử dụng một tính năng CSS mới, điều quan trọng là bạn phải hiểu được tác động của tính năng đó đối với hiệu suất của trang web, dù là tích cực hay tiêu cực. Giờ đây, @property đã có trong Đường cơ sở, bài đăng này sẽ khám phá tác động của @property đến hiệu suất và những việc bạn có thể làm để ngăn chặn tác động tiêu cực.

Đo điểm chuẩn hiệu suất của CSS bằng PerfTestRunner

Để đo điểm chuẩn hiệu suất của CSS, chúng tôi đã xây dựng bộ thử nghiệm "Điểm chuẩn bộ chọn CSS". Công cụ này được cung cấp bởi PerfTestRunner của Chromium và đo điểm chuẩn tác động của CSS đến hiệu suất. PerfTestRunner này là công cụ kết xuất cơ bản của Blink – Chromium – dùng cho các bài kiểm thử hiệu suất nội bộ.

Trình chạy bao gồm một phương thức measureRunsPerSecond dùng cho các chương trình kiểm thử. Số lần chạy mỗi giây càng cao thì càng tốt. Điểm chuẩn measureRunsPerSecond cơ bản với thư viện này có dạng như sau:

const testResults = PerfTestRunner.measureRunsPerSecond({
  "Test Description",
  iterationCount: 5,
  bootstrap: function() {
    // Code to execute before all iterations run
    // For example, you can inject a style sheet here
  },
  setup: function() {
    // Code to execute before a single iteration
  },
  run: function() {
    // The actual test that gets run and measured.
    // A typical test adjusts something on the page causing a style or layout invalidation
  },
  tearDown: function() {
    // Code to execute after a single iteration has finished
    // For example, undo DOM adjustments made within run()
  },
  done: function() {
    // Code to be run after all iterations have finished.
    // For example, remove the style sheets that were injected in the bootstrap phase
  },
});

Mỗi tuỳ chọn cho measureRunsPerSecond được mô tả thông qua các nhận xét trong khối mã, với hàm run là phần cốt lõi được đo lường.

Điểm chuẩn Bộ chọn CSS yêu cầu cây DOM

Vì hiệu suất của bộ chọn CSS cũng phụ thuộc vào kích thước của DOM, nên các điểm chuẩn này cần có một cây DOM có kích thước hợp lý. Thay vì tạo cây DOM này theo cách thủ công, cây này sẽ được tạo.

Ví dụ: hàm makeTree sau đây là một phần của điểm chuẩn @property. Hàm này xây dựng một cây gồm 1.000 phần tử, mỗi phần tử có một số phần tử con được lồng vào trong.

const $container = document.querySelector('#container');

function makeTree(parentEl, numSiblings) {
  for (var i = 0; i <= numSiblings; i++) {
    $container.appendChild(
      createElement('div', {
        className: `tagDiv wrap${i}`,
        innerHTML: `<div class="tagDiv layer1" data-div="layer1">
          <div class="tagDiv layer2">
            <ul class="tagUl">
              <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
            </ul>
          </div>
        </div>`,
      })
    );
  }
}

makeTree($container, 1000);

Vì điểm chuẩn bộ chọn CSS không sửa đổi cây DOM, nên quá trình tạo cây này chỉ được thực thi một lần, trước khi bất kỳ điểm chuẩn nào chạy.

Chạy phép đo điểm chuẩn

Để chạy phép đo điểm chuẩn trong bộ kiểm thử, trước tiên, bạn phải khởi động máy chủ web:

npm run start

Sau khi bắt đầu, bạn có thể truy cập điểm chuẩn tại URL đã xuất bản và thực thi window.startTest() theo cách thủ công.

Để chạy riêng các phép đo điểm chuẩn này mà không có bất kỳ tiện ích hoặc yếu tố nào khác can thiệp, Puppeteer được kích hoạt từ CLI để tải và thực thi phép đo điểm chuẩn đã truyền.

Đối với các điểm chuẩn @property này, thay vì truy cập trang liên quan tại URL của nó, http://localhost:3000/benchmarks/at-rule/at-property.html hãy gọi các lệnh sau trên CLI:

npm run benchmark at-rule/at-property

Thao tác này sẽ tải trang thông qua Puppeteer, tự động gọi window.startTest() và báo cáo lại kết quả.

Đo điểm chuẩn hiệu suất của các thuộc tính CSS

Để đo điểm chuẩn hiệu suất của một thuộc tính CSS, bạn đo điểm chuẩn tốc độ xử lý vô hiệu hoá kiểu và tác vụ tính toán lại kiểu tiếp theo mà trình duyệt cần thực hiện.

Vô hiệu hoá kiểu là quá trình đánh dấu phần tử nào cần tính toán lại kiểu của chúng để phản hồi thay đổi trong DOM. Cách đơn giản nhất có thể là vô hiệu hoá mọi thứ để phản hồi mọi thay đổi.

Khi làm như vậy, bạn cần phân biệt giữa các thuộc tính CSS kế thừa và các thuộc tính CSS không kế thừa.

  • Khi một thuộc tính CSS kế thừa các thay đổi trên một phần tử được nhắm mục tiêu, thì kiểu của tất cả các phần tử trong cây con bên dưới phần tử được nhắm mục tiêu cũng cần thay đổi.
  • Khi một thuộc tính CSS không kế thừa các thay đổi trên một phần tử được nhắm mục tiêu, thì chỉ các kiểu cho phần tử riêng lẻ đó mới bị vô hiệu.

Vì không công bằng khi so sánh các thuộc tính kế thừa với các thuộc tính không kế thừa, nên bạn có thể chạy hai bộ điểm chuẩn:

  • Một tập hợp điểm chuẩn có các thuộc tính kế thừa.
  • Một bộ điểm chuẩn có các thuộc tính không kế thừa.

Điều quan trọng là phải chọn cẩn thận những tài sản để đo điểm chuẩn. Mặc dù một số thuộc tính (chẳng hạn như accent-color) chỉ làm mất hiệu lực của các kiểu, nhưng có nhiều thuộc tính (chẳng hạn như writing-mode) cũng làm mất hiệu lực của các thuộc tính khác như bố cục hoặc sơn. Bạn muốn các thuộc tính chỉ vô hiệu hoá kiểu.

Để xác định điều này, hãy tra cứu trong danh sách thuộc tính CSS của Blink. Mỗi tài sản có một trường invalidate liệt kê những nội dung không hợp lệ.

Ngoài ra, bạn cũng cần chọn một thuộc tính không được đánh dấu là independent trong danh sách đó, vì việc đo điểm chuẩn cho một thuộc tính như vậy sẽ làm sai lệch kết quả. Các thuộc tính độc lập không có bất kỳ tác dụng phụ nào đối với các thuộc tính hoặc cờ khác. Khi chỉ có các thuộc tính độc lập thay đổi, Blink sử dụng một đường dẫn mã nhanh để sao chép kiểu của thành phần con và cập nhật các giá trị mới trong bản sao được sao chép đó. Phương pháp này nhanh hơn so với việc tính toán lại toàn bộ.

Đo điểm chuẩn hiệu suất của các thuộc tính CSS kế thừa

Nhóm điểm chuẩn đầu tiên tập trung vào các thuộc tính CSS kế thừa. Có 3 loại thuộc tính kế thừa để kiểm thử và so sánh với nhau:

  • Một thuộc tính thông thường kế thừa: accent-color.
  • Một thuộc tính tuỳ chỉnh chưa đăng ký: --unregistered.
  • Một thuộc tính tuỳ chỉnh được đăng ký bằng inherits: true: --registered.

Các thuộc tính tuỳ chỉnh chưa đăng ký sẽ được thêm vào danh sách này vì các thuộc tính này kế thừa theo mặc định.

Như đã đề cập trước đó, thuộc tính kế thừa được chọn cẩn thận để chỉ vô hiệu hoá các kiểu và không được đánh dấu là independent.

Đối với các thuộc tính tuỳ chỉnh đã đăng ký, chỉ những thuộc tính có chỉ số mô tả inherits được đặt thành true mới được kiểm thử trong lần chạy này. Phần mô tả inherits xác định liệu thuộc tính có kế thừa từ phần tử con hay không. Việc thuộc tính này được đăng ký thông qua CSS @property hay JavaScript CSS.registerProperty không quan trọng, vì bản thân việc đăng ký không phải là một phần của phép đo điểm chuẩn.

Điểm chuẩn

Như đã đề cập, trang chứa điểm chuẩn bắt đầu bằng cách tạo một cây DOM để trang có đủ nhiều nút để xem mọi tác động của các thay đổi.

Mỗi điểm chuẩn sẽ thay đổi giá trị của một thuộc tính, sau đó kích hoạt việc vô hiệu hoá kiểu. Về cơ bản, điểm chuẩn đo lường thời gian tính toán lại trang tiếp theo để đánh giá lại tất cả các kiểu không hợp lệ đó.

Sau khi hoàn tất một phép đo điểm chuẩn, mọi kiểu được chèn sẽ được đặt lại để có thể bắt đầu phép đo điểm chuẩn tiếp theo.

Ví dụ: điểm chuẩn đo lường hiệu suất của việc thay đổi kiểu của --registered sẽ có dạng như sau:

let i = 0;
PerfTestRunner.measureRunsPerSecond({
  description,
  iterationCount: 5,
  bootstrap: () => {
    setCSS(`@property --registered {
      syntax: "<number>";
      initial-value: 0;
      inherits: true;
    }`);
  },
  setup: function() {
    // NO-OP
  },
  run: function() {
    document.documentElement.style.setProperty('--registered', i);
    window.getComputedStyle(document.documentElement).getPropertyValue('--registered'); // Force style recalculation
    i = (i == 0) ? 1 : 0;
  },
  teardown: () => {
    document.documentElement.style.removeProperty('--registered');
  },
  done: (results) => {
    resetCSS();
    resolve(results);
  },
});

Phép đo điểm chuẩn kiểm thử các loại thuộc tính khác hoạt động theo cách tương tự nhưng có bootstrap trống vì không có thuộc tính nào để đăng ký.

Kết quả

Khi chạy các phép đo điểm chuẩn này với 20 lần lặp trên MacBook Pro 2021 (Apple M1 Pro) có RAM 16 GB, bạn sẽ nhận được các giá trị trung bình sau:

  • Thuộc tính thông thường kế thừa (accent-color): 163 lần chạy mỗi giây (= 6,13 mili giây mỗi lần chạy)
  • Thuộc tính tuỳ chỉnh chưa đăng ký (--unregistered): 256 lượt chạy mỗi giây (= 3,90 mili giây mỗi lượt chạy)
  • Thuộc tính tuỳ chỉnh đã đăng ký với inherits: true (--registered): 252 lần chạy/giây (= 3,96 mili giây mỗi lần chạy)

Trong nhiều lần chạy, phép đo điểm chuẩn sẽ cho ra kết quả tương tự.

Biểu đồ thanh cho thấy kết quả của các thuộc tính kế thừa. Số càng cao thì hiệu suất càng cao.
Hình: Biểu đồ thanh có kết quả cho các thuộc tính kế thừa. Số càng cao thì hiệu suất càng cao.

Kết quả cho thấy rằng việc đăng ký một tài sản tuỳ chỉnh có chi phí rất thấp so với việc không đăng ký tài sản tuỳ chỉnh đó. Các thuộc tính tuỳ chỉnh đã đăng ký và kế thừa sẽ chạy ở tốc độ 98% so với các thuộc tính tuỳ chỉnh chưa đăng ký. Theo số tuyệt đối, việc đăng ký thuộc tính tuỳ chỉnh sẽ làm tăng mức hao tổn 0,06 mili giây.

Đo điểm chuẩn hiệu suất của các thuộc tính CSS không kế thừa

Các thuộc tính tiếp theo cần đo điểm chuẩn là những thuộc tính không kế thừa. Tại đây, bạn chỉ có thể đo điểm chuẩn cho hai loại thuộc tính:

  • Một thuộc tính thông thường không kế thừa: z-index.
  • Một tài sản tuỳ chỉnh đã đăng ký với inherits: false: --registered-no-inherit.

Các thuộc tính tuỳ chỉnh chưa được đăng ký không thể tham gia điểm chuẩn này vì các thuộc tính đó luôn kế thừa.

Điểm chuẩn

Các điểm chuẩn rất giống với các trường hợp trước. Đối với kiểm thử bằng --registered-no-inherit, quy trình đăng ký thuộc tính sau đây được chèn vào giai đoạn bootstrap của phép đo điểm chuẩn:

@property --registered-no-inherit {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

Kết quả

Chạy các phép đo điểm chuẩn này với 20 lần lặp trên MacBook Pro 2021 (Apple M1 Pro) với RAM 16GB cho các kết quả trung bình sau:

  • Thuộc tính thông thường không kế thừa: 290.269 lần chạy mỗi giây (= 3,44 µ giây mỗi lần chạy)
  • Thuộc tính tuỳ chỉnh đã đăng ký không kế thừa: 214.110 lượt chạy mỗi giây (= 4,67 µ giây mỗi lượt chạy)

Kiểm thử được lặp lại qua nhiều lần chạy và đây là kết quả điển hình.

Biểu đồ thanh có kết quả cho các thuộc tính không kế thừa. Số càng cao thì hiệu suất càng cao.
Hình: Biểu đồ thanh cho kết quả của các thuộc tính không kế thừa. Số càng cao thì hiệu suất càng cao.

Điều nổi bật ở đây là các thuộc tính không kế thừa hoạt động nhanh hơn nhiều so với các thuộc tính kế thừa. Mặc dù điều này là điều dễ hiểu đối với các thuộc tính thông thường, nhưng cũng đúng đối với các thuộc tính tuỳ chỉnh.

  • Đối với các tài sản thông thường, số lần chạy đã tăng từ 163 lần chạy mỗi giây lên hơn 290 nghìn lần chạy mỗi giây, tăng hiệu suất lên 1780%!
  • Đối với các tài sản tuỳ chỉnh, số lần chạy đã tăng từ 252 lần chạy/giây lên hơn 214 nghìn lần chạy/giây, tăng hiệu suất lên 848%!

Điểm chính cần lưu ý là việc sử dụng inherits: false khi đăng ký thuộc tính tuỳ chỉnh sẽ có tác động đáng kể. Nếu có thể đăng ký thuộc tính tuỳ chỉnh bằng inherits: false, bạn nhất định nên làm như vậy.

Điểm chuẩn thưởng: nhiều lượt đăng ký tài sản tuỳ chỉnh

Một điều thú vị khác cần đo điểm chuẩn là tác động của việc có nhiều lượt đăng ký tài sản tuỳ chỉnh. Để làm như vậy, hãy chạy lại kiểm thử bằng --registered-no-inherit, trong đó --registered-no-inherit sẽ thực hiện trước 25.000 lượt đăng ký tài sản tuỳ chỉnh khác. Các thuộc tính tuỳ chỉnh này được dùng trên :root.

Các lượt đăng ký này được thực hiện trong bước setup của phép đo điểm chuẩn:

setup: () => {
  const propertyRegistrations = [];
  const declarations = [];

  for (let i = 0; i < 25000; i++) {
    propertyRegistrations.push(`@property --custom-${i} { syntax: "<number>"; initial-value: 0; inherits: true; }`);
    declarations.push(`--custom-${i}: ${Math.random()}`);
  }

  setCSS(`${propertyRegistrations.join("\n")}
  :root {
    ${declarations.join("\n")}
  }`);
},

Số lần chạy mỗi giây cho điểm chuẩn này rất giống với kết quả cho "Thuộc tính tuỳ chỉnh đã đăng ký không kế thừa" (214.110 lần chạy mỗi giây so với 213.158 lần chạy mỗi giây), nhưng đó không phải là phần thú vị cần xem xét. Xét cho cùng, việc thay đổi một thuộc tính tuỳ chỉnh sẽ không bị ảnh hưởng bởi các lượt đăng ký từ các tài sản khác.

Phần thú vị của thử nghiệm này là đo lường tác động của chính các lượt đăng ký. Khi chuyển sang DevTools, bạn có thể thấy rằng 25.000 lượt đăng ký thuộc tính tuỳ chỉnh có chi phí tính toán lại kiểu ban đầu là hơn 30ms một chút. Sau khi hoàn tất, sự hiện diện của những thông tin đăng ký này không còn ảnh hưởng gì đến vấn đề nữa.

Ảnh chụp màn hình Công cụ cho nhà phát triển, trong đó chi phí &quot;Tính toán lại kiểu&quot; để thực hiện 25.000 lượt đăng ký thuộc tính tuỳ chỉnh được làm nổi bật. Chú giải công cụ cho biết mất 32,42 mili giây
Hình: Ảnh chụp màn hình Công cụ cho nhà phát triển, trong đó chi phí "Tính toán lại kiểu" để thực hiện 25.000 lượt đăng ký thuộc tính tuỳ chỉnh được làm nổi bật. Chú giải công cụ cho biết đã mất 32.42ms

Kết luận và nội dung cần ghi nhớ

Tóm lại, có 3 điều cần lưu ý từ tất cả những điều này:

  • Việc đăng ký một thuộc tính tuỳ chỉnh bằng @property sẽ gây ra một chút hao tổn về hiệu suất. Chi phí này thường không đáng kể vì khi đăng ký tài sản tuỳ chỉnh, bạn có thể mở khoá toàn bộ tiềm năng của tài sản, điều mà bạn không thể làm được nếu không đăng ký.

  • Việc sử dụng inherits: false khi đăng ký một thuộc tính tuỳ chỉnh sẽ có tác động đáng kể. Khi đó, bạn sẽ không cho tài sản kế thừa. Do đó, khi giá trị của thuộc tính thay đổi, điều đó chỉ ảnh hưởng đến kiểu của phần tử phù hợp thay vì toàn bộ cây con.

  • Việc có ít hoặc nhiều lượt đăng ký @property không ảnh hưởng đến việc tính toán lại kiểu. Bạn chỉ phải trả một khoản phí nhỏ khi đăng ký, nhưng sau khi hoàn tất, bạn sẽ không phải trả thêm khoản phí nào nữa.