Loại bỏ hiện tượng giật để cải thiện hiệu suất hiển thị

Tom Wiltzius
Tom Wiltzius

Giới thiệu

Bạn muốn ứng dụng web của mình mang lại cảm giác phản hồi và mượt mà khi thực hiện ảnh động, hiệu ứng chuyển đổi và các hiệu ứng nhỏ khác trên giao diện người dùng. Khi đảm bảo những hiệu ứng này không có hiện tượng giật, sự khác biệt giữa quảng cáo "gốc" hoặc là một thứ rối mắt, không được trau chuốt.

Đây là bài đầu tiên trong loạt bài viết đề cập đến việc tối ưu hoá hiệu suất hiển thị trong trình duyệt. Để bắt đầu, chúng tôi sẽ đề cập đến lý do khiến hoạt ảnh mượt mà lại khó khăn và những gì cần phải đạt được để đạt được điều này cũng như một vài phương pháp đơn giản và hiệu quả nhất. Nhiều ý tưởng trong số này ban đầu được trình bày trong "Jank Busters" một bài nói chuyện của Nat Duca và tôi tại bài nói chuyện tại Google I/O (video) năm nay.

Giới thiệu về V-sync

Người chơi trò chơi trên máy tính có thể đã quen thuộc với thuật ngữ này, nhưng thuật ngữ này không phổ biến trên mạng: v-sync là gì?

Hãy xem xét màn hình của điện thoại: màn hình làm mới định kỳ, thường (nhưng không phải lúc nào cũng!) khoảng 60 lần một giây. V-sync (đồng bộ hoá theo chiều dọc) là hoạt động chỉ tạo khung hình mới giữa các lần làm mới màn hình. Bạn có thể coi đây là một tình huống tương tranh giữa quy trình ghi dữ liệu vào vùng đệm màn hình và hệ điều hành đọc dữ liệu đó để đưa dữ liệu lên màn hình. Chúng ta muốn nội dung của khung lưu vào vùng đệm thay đổi giữa các lần làm mới này chứ không phải trong thời gian đó; nếu không, màn hình sẽ hiển thị một nửa của một khung hình và một nửa của một khung hình khác, dẫn đến hiện tượng "xé hình".

Để có ảnh động mượt mà, bạn cần có một khung mới sẵn sàng mỗi khi làm mới màn hình. Điều này có hai ảnh hưởng lớn: thời gian kết xuất khung hình (tức là khi khung hình cần sẵn sàng) và ngân sách kết xuất khung hình (tức là thời gian trình duyệt phải tạo khung hình). Bạn chỉ có thời gian giữa các lần làm mới màn hình để hoàn tất một khung hình (khoảng 16 mili giây trên màn hình 60 Hz) và bạn muốn bắt đầu tạo khung hình tiếp theo ngay khi khung hình cuối cùng xuất hiện trên màn hình.

Thời gian là tất cả: requestAnimationFrame

Nhiều nhà phát triển web sử dụng setInterval hoặc setTimeout mỗi 16 mili giây để tạo ảnh động. Đây là vấn đề vì nhiều lý do (và chúng ta sẽ thảo luận thêm trong một phút), nhưng điều đặc biệt quan tâm là:

  • Độ phân giải của bộ tính giờ từ JavaScript chỉ theo thứ tự vài mili giây
  • Các thiết bị khác nhau có tốc độ làm mới khác nhau

Hãy nhớ lại vấn đề về thời gian kết xuất khung hình nêu trên: bạn cần một khung ảnh động hoàn chỉnh, được hoàn thiện bằng JavaScript, thao tác DOM, bố cục, tô màu, v.v. để sẵn sàng trước khi lần làm mới màn hình tiếp theo diễn ra. Độ phân giải của bộ tính giờ thấp có thể gây khó khăn cho việc hoàn tất khung ảnh động trước lần làm mới màn hình tiếp theo, nhưng sự thay đổi về tốc độ làm mới màn hình khiến không thể sử dụng bộ tính giờ cố định. Bất kể khoảng thời gian hẹn giờ là bao nhiêu, bạn cũng sẽ dần rời khỏi khung thời gian của một khung hình và để rồi bỏ lỡ một khung hình. Điều này sẽ xảy ra ngay cả khi bộ tính giờ được kích hoạt với độ chính xác là mili giây, điều này sẽ không xảy ra (như các nhà phát triển đã phát hiện) -- độ phân giải của bộ hẹn giờ thay đổi tùy thuộc vào việc máy đang chạy pin hay đang cắm điện, có thể bị ảnh hưởng bởi các thẻ trong nền đang sử dụng tài nguyên, v. v. Ngay cả khi điều này hiếm khi xảy ra (ví dụ: cứ 16 khung hình vì bạn đã tắt một phần nghìn giây) thì bạn sẽ nhận thấy: bạn sẽ thả vài khung hình một giây. Bạn cũng sẽ tạo ra các khung không bao giờ được hiển thị, điều này làm lãng phí năng lượng và thời gian của CPU mà bạn có thể đang dùng để làm những việc khác trong ứng dụng của mình.

Các màn hình khác nhau có tốc độ làm mới khác nhau: 60Hz là phổ biến, nhưng một số điện thoại là 59Hz, một số máy tính xách tay giảm xuống 50Hz ở chế độ tiết kiệm pin, một số màn hình máy tính để bàn là 70Hz.

Chúng tôi có xu hướng tập trung vào số khung hình/giây (FPS) khi thảo luận về hiệu suất kết xuất, nhưng sự chênh lệch có thể còn là một vấn đề thậm chí còn lớn hơn. Mắt chúng ta sẽ chú ý đến những điểm nối nhỏ, không đều trong hoạt ảnh mà hoạt ảnh có thời gian không hợp lý có thể tạo ra.

Cách để có được khung ảnh động được định thời gian chính xác là sử dụng requestAnimationFrame. Khi sử dụng API này, bạn đang yêu cầu trình duyệt cung cấp khung ảnh động. Lệnh gọi lại của bạn được gọi khi trình duyệt sắp tạo khung mới. Điều này xảy ra bất kể tốc độ làm mới là bao nhiêu.

requestAnimationFrame cũng có các thuộc tính đẹp khác:

  • Ảnh động trong các thẻ nền sẽ bị tạm dừng, giúp duy trì tài nguyên hệ thống và thời lượng pin.
  • Nếu hệ thống không thể xử lý việc kết xuất ở tốc độ làm mới màn hình, thì hệ thống có thể điều tiết ảnh động và tạo lệnh gọi lại ít thường xuyên hơn (ví dụ: 30 lần/giây trên màn hình 60 Hz). Mặc dù điều này làm giảm tốc độ khung hình xuống một nửa, nhưng nó vẫn giữ cho ảnh động nhất quán – và như đã trình bày ở trên, mắt chúng ta chú ý đến phương sai nhiều hơn so với tốc độ khung hình. Tần số 30 Hz ổn định sẽ trông đẹp hơn so với tần số 60 Hz mà bỏ lỡ một vài khung hình/giây.

requestAnimationFrame đã được thảo luận khắp nơi trên thế giới, vì vậy hãy tham khảo các bài viết như bài viết này của JS quảng cáo để biết thêm thông tin về chủ đề này. Tuy nhiên, đây là bước quan trọng đầu tiên để tạo ảnh động mượt mà.

Ngân sách khung

Vì chúng ta muốn một khung mới sẵn sàng trên mỗi lần làm mới màn hình, nên chỉ có một khoảng thời gian giữa các lần làm mới để thực hiện toàn bộ công việc tạo khung mới. Trên màn hình 60Hz, điều đó có nghĩa là chúng ta có khoảng 16 mili giây để chạy tất cả JavaScript, thực hiện bố cục, vẽ và bất cứ việc gì trình duyệt phải làm để lấy khung hình. Điều này có nghĩa là nếu JavaScript trong lệnh gọi lại requestAnimationFrame mất hơn 16 mili giây để chạy, thì bạn không có hy vọng tạo ra khung hình kịp thời để đồng bộ hoá v!

16 mili giây không phải là nhiều thời gian. May mắn là Công cụ dành cho nhà phát triển của Chrome có thể giúp bạn theo dõi nếu bạn vượt quá ngân sách khung hình trong quá trình gọi lại requestAnimationFrame.

Việc mở tiến trình Công cụ dành cho nhà phát triển và nhanh chóng ghi lại hoạt ảnh này cho thấy rằng chúng tôi đã vượt quá ngân sách khi tạo ảnh động. Trong Dòng thời gian, hãy chuyển sang "Khung" và xem:

Bản minh hoạ có quá nhiều bố cục
Bản minh hoạ có quá nhiều bố cục

Các lệnh gọi lại requestAnimationFrame (rAF) đó đang mất hơn 200 mili giây. Quá trình này diễn ra quá lâu nên không thể đánh dấu một khung hình cứ 16 mili giây một lần! Khi mở một trong những lệnh gọi lại rAF dài đó, bạn sẽ thấy điều gì đang diễn ra bên trong: trong trường hợp này là rất nhiều bố cục.

Video của Paul đi sâu vào chi tiết hơn về nguyên nhân cụ thể dẫn đến việc bố cục lại (tức là đang đọc scrollTop) và cách tránh điều này. Tuy nhiên, vấn đề ở đây là bạn có thể đi sâu vào lệnh gọi lại và điều tra xem yếu tố nào mất nhiều thời gian như vậy.

Bản minh hoạ được cập nhật với bố cục gọn gàng hơn nhiều
Bản minh hoạ mới với bố cục gọn gàng hơn nhiều

Hãy lưu ý thời gian kết xuất khung hình là 16 mili giây. Không gian trống đó trong khung chính là khoảng trống để bạn làm nhiều việc hơn (hoặc cho phép trình duyệt thực hiện công việc cần thiết trong nền). Khoảng trống đó là một việc tốt.

Nguồn khác gây ra hiện tượng giật

Nguyên nhân lớn nhất gây ra sự cố khi cố gắng chạy hoạt ảnh được hỗ trợ JavaScript những nội dung khác có thể cản trở lệnh gọi lại rAF và thậm chí ngăn chặn nó hoạt động chút nào. Ngay cả khi lệnh gọi lại rAF nhỏ gọn và chỉ chạy trong vài phút mili giây, các hoạt động khác (như xử lý XHR vừa được đăng nhập, chạy trình xử lý sự kiện đầu vào hoặc chạy bản cập nhật theo lịch trên một bộ tính giờ) có thể đột nhiên truy cập và chạy trong bất kỳ khoảng thời gian nào mà không qua lại. Trên thiết bị di động Đôi khi, việc xử lý các sự kiện này có thể mất hàng trăm mili giây, trong lúc đó hoạt ảnh của bạn sẽ ngừng hoạt động hoàn toàn. Chúng tôi gọi những ảnh động liên tục giật.

Không có giải pháp nào giúp bạn tránh những tình huống này, nhưng có một số phương pháp hay nhất về kiến trúc giúp bạn đạt được thành công:

  • Không xử lý nhiều trong các trình xử lý đầu vào! Việc thực hiện nhiều JS hoặc cố gắng sắp xếp lại toàn bộ trang trong khi ví dụ: trình xử lý onscroll là nguyên nhân rất phổ biến gây ra hiện tượng giật kinh hoàng.
  • Đẩy càng nhiều quá trình xử lý (đọc: bất kỳ nội dung nào sẽ mất nhiều thời gian để chạy) vào lệnh gọi lại rAF hoặc Trình chạy web càng tốt.
  • Nếu bạn đẩy công việc vào lệnh gọi lại rAF, hãy cố gắng chia nhỏ để bạn chỉ xử lý một chút mỗi khung hình hoặc trì hoãn nó cho đến khi một hoạt ảnh quan trọng kết thúc - bằng cách này bạn có thể tiếp tục chạy các lệnh gọi lại rAF ngắn và tạo ảnh động mượt mà.

Để xem hướng dẫn tuyệt vời về cách đẩy quá trình xử lý vào các lệnh gọi lại requestAnimationFrame thay vì trình xử lý đầu vào, hãy xem bài viết của Paul Lewis với tư cách Leaner, Meaner, nhanh hơn với requestAnimationFrame.

Ảnh động CSS

Điều gì tốt hơn JS nhẹ trong các lệnh gọi lại sự kiện và rAF của bạn? Không có JS.

Trước đó, chúng tôi đã nói không có giải pháp nào giúp tránh làm gián đoạn lệnh gọi lại rAF, nhưng bạn có thể sử dụng ảnh động CSS để hoàn toàn không cần đến chúng. Trên Chrome dành cho Android nói riêng (và các trình duyệt khác đang hoạt động trên các tính năng tương tự), ảnh động CSS có thuộc tính rất mong muốn mà trình duyệt thường có thể chạy chúng ngay cả khi JavaScript đang chạy.

Có một tuyên bố ngầm ẩn trong phần trên về hiện tượng giật: các trình duyệt chỉ có thể thực hiện một việc tại một thời điểm. Điều này không hoàn toàn đúng, nhưng đó là một giả định hữu ích nếu áp dụng: tại bất kỳ thời điểm nào trình duyệt có thể chạy JS, thực hiện bố cục hoặc tô màu, nhưng chỉ một lần tại một thời điểm. Bạn có thể xác minh điều này trong chế độ xem Dòng thời gian của Công cụ dành cho nhà phát triển. Một trong những ngoại lệ đối với quy tắc này là hoạt ảnh CSS trên Chrome dành cho Android (và sắp tới là Chrome dành cho máy tính để bàn).

Nếu có thể, việc sử dụng ảnh động CSS sẽ vừa đơn giản hoá ứng dụng của bạn vừa giúp ảnh động chạy mượt mà, ngay cả khi JavaScript chạy.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Nếu bạn nhấp vào nút, JavaScript sẽ chạy trong 180 mili giây, gây ra hiện tượng giật. Nhưng thay vào đó, nếu chúng ta điều khiển ảnh động đó bằng ảnh động CSS thì hiện tượng giật sẽ không còn xảy ra nữa.

(Hãy nhớ rằng tại thời điểm viết bài này, hoạt ảnh CSS chỉ không bị giật trên Chrome dành cho Android chứ không phải Chrome dành cho máy tính để bàn).

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

Để biết thêm thông tin về cách sử dụng Ảnh động CSS, hãy xem các bài viết như bài viết này về MDN.

Tổng kết

Nội dung ngắn gọn:

  1. Khi tạo ảnh động, việc tạo khung hình cho mỗi lần làm mới màn hình là rất quan trọng. Ảnh động của Vsync có tác động tích cực rất lớn đến cảm nhận của người dùng trong ứng dụng.
  2. Cách tốt nhất để tải ảnh động vsync trong Chrome và các trình duyệt hiện đại khác là để sử dụng ảnh động CSS. Khi bạn cần sự linh hoạt hơn so với ảnh động CSS cung cấp, kỹ thuật tốt nhất là ảnh động dựa trên requestAnimationFrame.
  3. Để các ảnh động rAF luôn hoạt động bình thường và vui vẻ, hãy đảm bảo các trình xử lý sự kiện khác không cản trở lệnh gọi lại rAF đang chạy và duy trì lệnh gọi lại rAF ngắn (<15 mili giây).

Cuối cùng, ảnh động vsync'd không chỉ áp dụng cho ảnh động giao diện người dùng đơn giản mà còn áp dụng cho ảnh động Canvas2D, ảnh động WebGL và thậm chí cả thao tác cuộn trên các trang tĩnh. Trong bài viết tiếp theo của loạt bài này, chúng ta sẽ tìm hiểu kỹ hơn về hiệu suất cuộn dựa trên các khái niệm này.

Chúc bạn tạo ảnh động vui vẻ!

Tài liệu tham khảo