Hoạt hình Worklet của Houdini

Tăng tốc độ cho ảnh động của ứng dụng web

Tóm tắt: Animation Worklet cho phép bạn viết các ảnh động bắt buộc chạy ở tốc độ khung hình gốc của thiết bị để có độ mượt mà không bị giật™ hơn, giúp ảnh động của bạn có khả năng chống lại tình trạng giật trên luồng chính và có thể liên kết với thao tác cuộn thay vì thời gian. Animation Worklet có trong Chrome Canary (đằng sau cờ "Các tính năng thử nghiệm của nền tảng web") và chúng tôi đang lên kế hoạch cho một Thử nghiệm theo nguyên gốc cho Chrome 71. Bạn có thể bắt đầu sử dụng tính năng này dưới dạng một tính năng nâng cao tăng dần ngay hôm nay.

Một Animation API khác?

Không phải. Đây là một phần mở rộng của những gì chúng tôi đã có và có lý do chính đáng! Hãy bắt đầu từ đầu. Nếu muốn tạo hiệu ứng cho bất kỳ phần tử DOM nào trên web hiện nay, bạn có 2 lựa chọn rưỡi: Hiệu ứng chuyển đổi CSS cho hiệu ứng chuyển đổi đơn giản từ A sang B, Hiệu ứng động CSS cho hiệu ứng động dựa trên thời gian phức tạp hơn, có khả năng theo chu kỳ và Web Animations API (WAAPI) cho hiệu ứng động phức tạp gần như tuỳ ý. Ma trận hỗ trợ của WAAPI có vẻ khá ảm đạm, nhưng đang trên đà phát triển. Cho đến lúc đó, sẽ có một polyfill.

Điểm chung của tất cả các phương thức này là chúng không trạng thái và dựa trên thời gian. Nhưng một số hiệu ứng mà nhà phát triển đang thử không phải là hiệu ứng dựa trên thời gian cũng không phải là hiệu ứng không trạng thái. Ví dụ: trình cuộn thị sai khét tiếng là trình cuộn dựa trên thao tác cuộn, như tên gọi của nó. Việc triển khai một trình cuộn thị sai hiệu quả trên web ngày nay khó hơn bạn tưởng.

Còn tính phi trạng thái thì sao? Ví dụ: hãy nghĩ về thanh địa chỉ của Chrome trên Android. Nếu bạn cuộn xuống, phần này sẽ cuộn ra khỏi khung hiển thị. Nhưng ngay khi bạn cuộn lên, thanh này sẽ xuất hiện lại, ngay cả khi bạn đang ở giữa trang đó. Ảnh động này không chỉ phụ thuộc vào vị trí cuộn mà còn phụ thuộc vào hướng cuộn trước đó của bạn. Đây là trạng thái.

Một vấn đề khác là tạo kiểu cho thanh cuộn. Chúng nổi tiếng là không thể tạo kiểu hoặc ít nhất là không đủ khả năng tạo kiểu. Nếu tôi muốn thanh cuộn là chú mèo nyan thì sao? Dù bạn chọn kỹ thuật nào, việc tạo một thanh cuộn tuỳ chỉnh cũng không mang lại hiệu suất cao và cũng không dễ dàng.

Vấn đề là tất cả những điều này đều khó xử và khó có thể triển khai một cách hiệu quả. Hầu hết các thành phần này đều dựa vào các sự kiện và/hoặc requestAnimationFrame, có thể giữ bạn ở tốc độ 60 khung hình/giây, ngay cả khi màn hình của bạn có khả năng chạy ở tốc độ 90 khung hình/giây, 120 khung hình/giây trở lên và sử dụng một phần nhỏ ngân sách khung hình luồng chính quý giá của bạn.

Animation Worklet mở rộng các chức năng của ngăn xếp ảnh động trên web để giúp bạn dễ dàng tạo những loại hiệu ứng này. Trước khi bắt đầu, hãy đảm bảo rằng chúng ta đã nắm được những kiến thức cơ bản về ảnh động.

Thông tin cơ bản về ảnh động và dòng thời gian

WAAPI và Animation Worklet sử dụng rộng rãi các dòng thời gian để cho phép bạn điều phối các hiệu ứng động và hiệu ứng theo cách bạn muốn. Phần này là phần giới thiệu hoặc ôn tập nhanh về dòng thời gian và cách dòng thời gian hoạt động với ảnh động.

Mỗi tài liệu đều có document.timeline. Giá trị này bắt đầu từ 0 khi tài liệu được tạo và đếm số mili giây kể từ khi tài liệu bắt đầu tồn tại. Tất cả ảnh động của một tài liệu đều hoạt động tương ứng với dòng thời gian này.

Để hiểu rõ hơn, hãy xem đoạn mã WAAPI này

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Khi chúng ta gọi animation.play(), ảnh động sẽ sử dụng currentTime của dòng thời gian làm thời gian bắt đầu. Ảnh động của chúng ta có độ trễ là 3000 mili giây, tức là ảnh động sẽ bắt đầu (hoặc trở thành "đang hoạt động") khi dòng thời gian đạt đến `startTime

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. Vấn đề là dòng thời gian kiểm soát vị trí của chúng ta trong ảnh động!

Sau khi đạt đến khung hình chính cuối cùng, ảnh động sẽ quay lại khung hình chính đầu tiên và bắt đầu lần lặp lại tiếp theo của ảnh động. Quá trình này lặp lại tổng cộng 3 lần kể từ khi chúng tôi đặt iterations: 3. Nếu muốn ảnh động không bao giờ dừng, chúng ta sẽ viết iterations: Number.POSITIVE_INFINITY. Sau đây là kết quả của mã ở trên.

WAAPI có sức mạnh đáng kinh ngạc và có nhiều tính năng khác trong API này, chẳng hạn như làm mịn, độ lệch bắt đầu, trọng số khung hình chính và hành vi điền sẽ vượt quá phạm vi của bài viết này. Nếu muốn tìm hiểu thêm, bạn nên đọc bài viết này về CSS Animations trên CSS Tricks.

Viết Animation Worklet

Giờ đây, khi đã nắm được khái niệm về dòng thời gian, chúng ta có thể bắt đầu xem xét Animation Worklet và cách công cụ này cho phép bạn tuỳ chỉnh dòng thời gian! API Animation Worklet không chỉ dựa trên WAAPI mà còn là một nguyên tắc cơ bản ở cấp thấp hơn (theo nghĩa của web có thể mở rộng) giải thích cách hoạt động của các hàm WAAPI. Về cú pháp, chúng rất giống nhau:

Worklet ảnh động WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

Điểm khác biệt nằm ở tham số đầu tiên, đó là tên của worklet điều khiển ảnh động này.

Phát hiện đối tượng

Chrome là trình duyệt đầu tiên cung cấp tính năng này, vì vậy, bạn cần đảm bảo mã của mình không chỉ mong đợi AnimationWorklet xuất hiện. Vì vậy, trước khi tải worklet, chúng ta nên phát hiện xem trình duyệt của người dùng có hỗ trợ AnimationWorklet hay không bằng một thao tác kiểm tra đơn giản:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Đang tải một worklet

Worklet là một khái niệm mới do nhóm đặc trách Houdini giới thiệu để giúp bạn dễ dàng tạo và mở rộng quy mô nhiều API mới. Chúng ta sẽ đề cập đến thông tin chi tiết về worklet sau, nhưng để đơn giản, bạn có thể coi chúng là các luồng nhẹ và có chi phí thấp (giống như worker) ở thời điểm hiện tại.

Chúng ta cần đảm bảo đã tải một worklet có tên "passthrough" trước khi khai báo ảnh động:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Điều gì đang xảy ra ở đây? Chúng ta đang đăng ký một lớp làm trình tạo ảnh động bằng cách sử dụng lệnh gọi registerAnimator() của AnimationWorklet, đặt tên cho lớp đó là "passthrough". Đây là tên mà chúng ta đã dùng trong hàm khởi tạo WorkletAnimation() ở trên. Sau khi quá trình đăng ký hoàn tất, lời hứa do addModule() trả về sẽ phân giải và chúng ta có thể bắt đầu tạo ảnh động bằng cách sử dụng worklet đó.

Phương thức animate() của phiên bản sẽ được gọi cho mọi khung hình mà trình duyệt muốn kết xuất, truyền currentTime của dòng thời gian hoạt ảnh cũng như hiệu ứng hiện đang được xử lý. Chúng ta chỉ có một hiệu ứng, đó là KeyframeEffect và chúng ta đang dùng currentTime để đặt localTime của hiệu ứng, đó là lý do tại sao trình tạo ảnh động này được gọi là "passthrough" (truyền qua). Với mã này cho worklet, WAAPI và AnimationWorklet ở trên hoạt động hoàn toàn giống nhau, như bạn có thể thấy trong bản minh hoạ.

Thời gian

Tham số currentTime của phương thức animate()currentTime của dòng thời gian mà chúng ta đã truyền đến hàm khởi tạo WorkletAnimation(). Trong ví dụ trước, chúng ta chỉ truyền thời gian đó qua hiệu ứng. Nhưng vì đây là mã JavaScript nên chúng ta có thể bóp méo thời gian 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Chúng ta lấy Math.sin() của currentTime và ánh xạ lại giá trị đó vào phạm vi [0; 2000], đây là phạm vi thời gian mà hiệu ứng của chúng ta được xác định. Giờ đây, ảnh động trông rất khác mà không cần thay đổi khung hình chính hoặc các lựa chọn của ảnh động. Mã worklet có thể phức tạp tuỳ ý và cho phép bạn xác định theo phương thức lập trình những hiệu ứng được phát theo thứ tự nào và ở mức độ nào.

Nhiều lựa chọn

Bạn có thể muốn sử dụng lại một thành phần và thay đổi các con số của thành phần đó. Vì lý do này, hàm khởi tạo WorkletAnimation cho phép bạn truyền một đối tượng tuỳ chọn đến worklet:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

Trong ví dụ này, cả hai ảnh động đều được điều khiển bằng cùng một mã, nhưng có các lựa chọn khác nhau.

Cho tôi biết tiểu bang nơi bạn sinh sống!

Như tôi đã đề cập trước đó, một trong những vấn đề chính mà animation worklet hướng đến là ảnh động có trạng thái. Các worklet ảnh động được phép giữ trạng thái. Tuy nhiên, một trong những tính năng cốt lõi của worklet là chúng có thể được di chuyển sang một luồng khác hoặc thậm chí bị huỷ để tiết kiệm tài nguyên, điều này cũng sẽ huỷ trạng thái của chúng. Để ngăn mất trạng thái, animation worklet cung cấp một hook có tên là before (trước khi) một worklet bị huỷ. Bạn có thể dùng hook này để trả về một đối tượng trạng thái. Đối tượng đó sẽ được truyền đến hàm khởi tạo khi worklet được tạo lại. Khi tạo ban đầu, tham số đó sẽ là undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Mỗi lần làm mới bản minh hoạ này, bạn sẽ có 50% cơ hội thấy hình vuông xoay theo hướng nào. Nếu trình duyệt huỷ worklet và di chuyển worklet đó sang một luồng khác, thì sẽ có một lệnh gọi Math.random() khác khi tạo, điều này có thể gây ra sự thay đổi đột ngột về hướng. Để đảm bảo điều đó không xảy ra, chúng ta sẽ trả về hướng được chọn ngẫu nhiên của các ảnh động dưới dạng state và sử dụng hướng đó trong hàm tạo (nếu được cung cấp).

Tạo hiệu ứng cho dòng thời gian: ScrollTimeline

Như phần trước đã trình bày, AnimationWorklet cho phép chúng ta xác định theo phương thức lập trình cách tiến trình trên dòng thời gian ảnh hưởng đến hiệu ứng của ảnh động. Nhưng cho đến nay, dòng thời gian của chúng tôi luôn là document.timeline, theo dõi thời gian.

ScrollTimeline mở ra những khả năng mới và cho phép bạn điều khiển các ảnh động bằng thao tác cuộn thay vì thời gian. Chúng ta sẽ sử dụng lại worklet "truyền qua" đầu tiên cho bản minh hoạ này:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Thay vì truyền document.timeline, chúng ta sẽ tạo một ScrollTimeline mới. Có lẽ bạn đã đoán ra, ScrollTimeline không sử dụng thời gian mà sử dụng vị trí cuộn của scrollSource để đặt currentTime trong worklet. Khi được cuộn lên trên cùng (hoặc sang trái), currentTime = 0 sẽ được đặt, còn khi được cuộn xuống dưới cùng (hoặc sang phải), currentTime sẽ được đặt thành timeRange. Nếu di chuyển hộp trong bản minh hoạ này, bạn có thể kiểm soát vị trí của hộp màu đỏ.

Nếu bạn tạo một ScrollTimeline có phần tử không cuộn, thì currentTime của dòng thời gian sẽ là NaN. Vì vậy, đặc biệt khi thiết kế thích ứng, bạn luôn phải chuẩn bị cho NaN dưới dạng currentTime. Thông thường, bạn nên đặt giá trị mặc định là 0.

Liên kết ảnh động với vị trí cuộn là điều mà người dùng đã tìm kiếm từ lâu, nhưng chưa bao giờ thực sự đạt được mức độ trung thực này (ngoài các giải pháp tạm thời bằng CSS3D). Animation Worklet cho phép triển khai các hiệu ứng này một cách đơn giản mà vẫn có hiệu suất cao. Ví dụ: hiệu ứng cuộn thị sai như bản minh hoạ này cho thấy rằng giờ đây, bạn chỉ cần một vài dòng để xác định ảnh động dựa trên thao tác cuộn.

Tìm hiểu sâu

Worklet

Worklet là các ngữ cảnh JavaScript có phạm vi riêng biệt và giao diện API rất nhỏ. Bề mặt API nhỏ cho phép trình duyệt tối ưu hoá mạnh mẽ hơn, đặc biệt là trên các thiết bị tầm thấp. Ngoài ra, các worklet không bị ràng buộc với một vòng lặp sự kiện cụ thể, nhưng có thể di chuyển giữa các luồng khi cần. Điều này đặc biệt quan trọng đối với AnimationWorklet.

Compositor NSync

Bạn có thể biết rằng một số thuộc tính CSS có tốc độ hoạt ảnh nhanh, trong khi những thuộc tính khác thì không. Một số thuộc tính chỉ cần một số thao tác trên GPU để được tạo hiệu ứng động, trong khi những thuộc tính khác buộc trình duyệt phải bố trí lại toàn bộ tài liệu.

Trong Chrome (cũng như nhiều trình duyệt khác), chúng tôi có một quy trình gọi là trình kết hợp. Nhiệm vụ của quy trình này là sắp xếp các lớp và hoạ tiết, sau đó sử dụng GPU để cập nhật màn hình thường xuyên nhất có thể, lý tưởng nhất là nhanh nhất có thể (thường là 60 Hz). Tuỳ thuộc vào thuộc tính CSS đang được tạo ảnh động, trình duyệt có thể chỉ cần trình kết hợp thực hiện công việc của trình kết hợp, trong khi các thuộc tính khác cần chạy bố cục. Đây là một thao tác mà chỉ luồng chính mới có thể thực hiện. Tuỳ thuộc vào thuộc tính mà bạn dự định tạo ảnh động, animation worklet của bạn sẽ được liên kết với luồng chính hoặc chạy trong một luồng riêng biệt đồng bộ hoá với trình kết hợp.

Cảnh cáo nhẹ

Thường chỉ có một quy trình trình kết hợp có thể được chia sẻ trên nhiều thẻ, vì GPU là một tài nguyên có tính cạnh tranh cao. Nếu trình kết hợp bị chặn theo cách nào đó, toàn bộ trình duyệt sẽ dừng hoạt động và không phản hồi thông tin đầu vào của người dùng. Bạn cần phải tránh điều này bằng mọi giá. Vậy điều gì sẽ xảy ra nếu worklet của bạn không thể phân phối dữ liệu mà thành phần kết hợp cần đúng lúc để khung hình được kết xuất?

Nếu điều này xảy ra, worklet sẽ được phép "trượt" (theo quy cách). Nó nằm sau trình tổng hợp và trình tổng hợp được phép sử dụng lại dữ liệu của khung hình cuối cùng để duy trì tốc độ khung hình. Về mặt hình ảnh, điều này sẽ trông giống như hiện tượng giật hình, nhưng điểm khác biệt lớn là trình duyệt vẫn phản hồi hoạt động đầu vào của người dùng.

Kết luận

AnimationWorklet có nhiều khía cạnh và mang lại nhiều lợi ích cho web. Lợi ích rõ ràng là bạn có nhiều quyền kiểm soát hơn đối với các ảnh động và có những cách mới để điều khiển ảnh động nhằm mang lại độ trung thực về hình ảnh ở cấp độ mới cho web. Tuy nhiên, thiết kế API này cũng cho phép bạn làm cho ứng dụng của mình có khả năng chống lại hiện tượng giật hình tốt hơn trong khi vẫn có quyền truy cập vào tất cả các tính năng mới cùng một lúc.

Animation Worklet có trong Canary và chúng tôi đang hướng đến một Bản dùng thử nguồn gốc với Chrome 71. Chúng tôi rất mong chờ những trải nghiệm mới tuyệt vời của bạn trên web và muốn biết những điểm mà chúng tôi có thể cải thiện. Ngoài ra, còn có một polyfill (bản vá lỗi) cung cấp cho bạn cùng một API, nhưng không cung cấp khả năng cách ly hiệu suất.

Xin lưu ý rằng CSS Transitions và CSS Animations vẫn là các lựa chọn hợp lệ và có thể đơn giản hơn nhiều đối với các ảnh động cơ bản. Nhưng nếu bạn cần làm cho mọi thứ trở nên bắt mắt, AnimationWorklet sẽ giúp bạn!