Hoạt hình Worklet của Houdini

Tăng hiệu quả cho ảnh động trong ứng dụng web

Tóm tắt: Animation Worklet cho phép bạn viết ả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 giật™, giúp ảnh động của bạn linh hoạt hơn trước tình trạng giật 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 (dưới cờ "Tính năng nền tảng web thử nghiệm") và chúng tôi đang lên kế hoạch cho một Bản dùng thử 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 cải tiến dần ngay hôm nay.

Một API Ảnh động khác?

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

Điểm chung của tất cả các phương thức này là không có trạng thái và được điều khiển theo thời gian. Tuy nhiên, một số hiệu ứng mà nhà phát triển đang thử không chạy theo thời gian cũng như không có trạng thái. Ví dụ: thanh cuộn thị sai khét tiếng, như tên gọi cho thấy, là thanh cuộn do người dùng điều khiển. Triển khai một thanh cuộn có hiệu suất cao trên web hiện nay là một việc khó khăn đáng ngạc nhiên.

Còn tính chất không có trạng thái thì sao? Hãy nghĩ đến thanh địa chỉ của Chrome trên Android, chẳng hạn. 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, nó sẽ quay lại, ngay cả khi bạn đang ở giữa trang đó. Ảnh động 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. Đó là có trạng thái.

Một vấn đề khác là định kiểu thanh cuộn. Những quảng cáo này nổi tiếng là không cách điệu — hoặc ít nhất là không đủ kiểu. Nếu tôi muốn mèo nyan làm thanh cuộn thì sao? Bất kể bạn chọn kỹ thuật nào, việc tạo thanh cuộn tuỳ chỉnh đều không hiệu quả và dễ dàng.

Vấn đề là tất cả những việc này đều rắc rối và khó có thể triển khai hiệu quả. Hầu hết các ứng dụng đều dựa vào các sự kiện và/hoặc requestAnimationFrame, có thể giúp bạn duy trì tốc độ 60 khung hình/giây, ngay cả khi màn hình có thể 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ỏ trong ngân sách khung hình luồng chính quý giá của bạn.

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

Giới thiệu về ảnh động và dòng thời gian

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

Mỗi tài liệu có document.timeline. Giá trị này bắt đầu từ 0 khi tài liệu được tạo và tính 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 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 sau

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 tiến trình làm thời gian bắt đầu. Ảnh động của chúng ta có độ trễ 3000 mili giây, nghĩa 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`. Điểm quan trọng là tiến trình kiểm soát vị trí của chúng ta trong ảnh động!

Khi ảnh động đạ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 vòng lặp 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 ta đặt iterations: 3. Nếu muốn ảnh động không bao giờ dừng lại, chúng ta sẽ viết iterations: Number.POSITIVE_INFINITY. Dưới đây là kết quả của mã ở trên.

WAAPI vô cùng mạnh mẽ và có nhiều tính năng khác trong API này như tốc độ, độ lệch bắt đầu, trọng số khung hình chính và hành vi tô màu sẽ làm vượt 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ề Ảnh động CSS trên CSS Tricks.

Viết Worklet ảnh động

Giờ đây, khi đã nắm được khái niệm về tiến trình, chúng ta có thể bắt đầu xem xét Worklet ảnh động và cách Worklet này cho phép bạn can thiệp vào tiến trình! API Worklet ảnh động không chỉ dựa trên WAAPI, mà còn theo nghĩa web có thể mở rộng – một dữ liệu nguyên gốc cấp thấp hơn giải thích cách hoạt động của WAAPI. Về cú pháp, các hàm này 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();
        

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

Phát hiện tính năng

Chrome là trình duyệt đầu tiên tích hợ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ỉ có 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 bước kiểm tra đơn giản:

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

Tải một worklet

Worklet là một khái niệm mới do nhóm tác vụ Houdini giới thiệu để giúp nhiều API mới dễ dàng xây dựng và mở rộng quy mô hơn. Chúng ta sẽ đề cập chi tiết hơn về các worklet sau, nhưng để đơn giản, bạn có thể coi chúng là các luồng giá rẻ và gọn nhẹ (như worker) trong thời gian này.

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 sẽ đă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 đã sử dụng trong hàm khởi tạo WorkletAnimation() ở trên. Sau khi hoàn tất quá trình đăng ký, 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 thực thể sẽ được gọi cho mỗi khung mà trình duyệt muốn kết xuất, truyền currentTime của tiến trình ảnh động cũng như hiệu ứng đang được xử lý. Chúng ta chỉ có một hiệu ứng là KeyframeEffect và đang sử dụng currentTime để đặt localTime của hiệu ứng, do đó, 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 giống hệt nhau, như bạn có thể thấy trong màn hình minh hoạ.

Thời gian

Tham số currentTime của phương thức animate()currentTime của tiến trình mà chúng ta đã truyền vào hàm khởi tạo WorkletAnimation(). Trong ví dụ trước, chúng ta chỉ truyền thời gian đó đến hiệu ứng. Nhưng vì đây là mã JavaScript nên chúng ta có thể biến dạng 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 khoảng [0; 2000]. Đây là khoảng thời gian mà hiệu ứng của chúng ta được xác định. Bây giờ, ảnh động trông rất khác mà không cần thay đổi các khung hình chính hoặc các tuỳ chọn của ảnh động. Mã worklet có thể phức tạp tuỳ ý và cho phép bạn xác định hiệu ứng nào được phát theo thứ tự nào và ở mức độ nào theo phương thức lập trình.

Tuỳ chọn trên Tuỳ chọn

Bạn có thể muốn sử dụng lại một worklet và thay đổi số lượng của worklet đó. 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 tuỳ chọn khác nhau.

Cho tôi biết trạng thái cục bộ của bạn!

Như tôi đã gợi ý trước đó, một trong những vấn đề chính mà công cụ ảnh động hướng đến giải quyết là ảnh động trạng thái. Các công việc ả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 các 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 các worklet. Để ngăn tình trạng mất trạng thái, công cụ ảnh động cung cấp một trình nối được gọi trước khi công cụ ảnh động bị huỷ mà bạn có thể dùng để trả về đối tượng trạng thái. Đối tượng đó sẽ được truyền đến hàm khởi tạo khi tạo lại công việc. 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 khi làm mới bản minh hoạ này, bạn có 50/50 cơ hội để biết hình vuông sẽ xoay theo hướng nào. Nếu trình duyệt chia nhỏ worklet và di chuyển 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 trả về hướng ảnh động được chọn ngẫu nhiên dưới dạng trạng thái và sử dụng hướng đó trong hàm khởi tạo (nếu có).

Đưa vào không gian không gian-thời gian: ScrollDòng thời gian

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

ScrollTimeline mở ra nhiều khả năng mới và cho phép bạn điều khiển ảnh động bằng cách cuộn thay vì thời gian. Chúng tôi sẽ sử dụng lại worklet "thông qua" đầu tiên của mình 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ó thể bạn đã đoán được, 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 di chuyển đến đầu (hoặc sang trái) nghĩa là currentTime = 0, còn khi di chuyển đến cuối (hoặc sang phải) thì currentTime sẽ được đặt thành timeRange. Nếu cuộ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 ScrollTimeline có một phần tử không cuộn, thì currentTime của tiến trình sẽ là NaN. Vì vậy, đặc biệt là khi thiết kế thích ứng, bạn phải luôn chuẩn bị sẵn NaN làm currentTime. Thông thường, việc đặt mặc định thành giá trị 0 là có thể hiểu được.

Việc liên kết ảnh động với vị trí cuộn là điều mà chúng tôi đã 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 hacky với 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 mang lại hiệu suất cao. Ví dụ: hiệu ứng cuộn theo hiệu ứng thị sai như màn hình 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 do cuộn.

Tìm hiểu sâu

Worklet

Worklet là ngữ cảnh JavaScript có phạm vi riêng biệt và giao diện API rất nhỏ. Giao diện API nhỏ cho phép tối ưu hoá mạnh mẽ hơn từ trình duyệt, đặc biệt là trên các thiết bị cấp 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 nếu cần. Điều này đặc biệt quan trọng đối với AnimationWorklet.

NSync của bộ tổng hợp

Bạn có thể biết rằng một số thuộc tính CSS nhất định có tốc độ tạo ảnh động nhanh trong khi các 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 để tạo ảnh động, trong khi một số 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ư trong nhiều trình duyệt khác), chúng ta có một quy trình được gọi là trình kết hợp, công việc của trình kết hợp là sắp xếp các lớp và hiệu ứng kết cấu, sau đó sử dụng GPU để cập nhật màn hình thường xuyên nhất có thể, tốt nhất là nhanh nhất có thể cập nhật màn hình (thường là 60Hz). Tuỳ thuộc vào thuộc tính CSS nào đang được tạo ảnh động, trình duyệt có thể chỉ cần có 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à 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 nào bạn dự định tạo ảnh động, công việc ảnh động 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ộ với trình kết hợp.

Cảnh cáo

Thường thì chỉ có một quy trình tổng hợp có thể được chia sẻ trên nhiều thẻ, vì GPU là một tài nguyên có mức độ 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ẽ bị dừng và không phản hồi hoạt động đầu vào của người dùng. Bạ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 không thể phân phối dữ liệu mà trình tổng hợp cần kịp thời để kết xuất khung hình?

Nếu điều này xảy ra, worklet được phép "trượt" theo thông số kỹ thuật. 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 gần nhất để 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, 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 ảnh động và các cách mới để thúc đẩy ảnh động nhằm mang lại độ trung thực hình ảnh mới cho web. Tuy nhiên, thiết kế API cũng cho phép bạn tăng khả năng chống giật của ứng dụng trong khi vẫn tiếp cận được tất cả những tính năng mới cùng một lúc.

Animation Worklet đang ở Canary và chúng tôi đang nhắm đến một Bản dùng thử theo nguyên gốc với Chrome 71. Chúng tôi đang háo hức chờ đợi các trải nghiệm web mới tuyệt vời của bạn và nghe về những điều chúng tôi có thể cải thiện. Ngoài ra, còn có một polyfill cung cấp cho bạn cùng một API, nhưng không cung cấp tính năng tách biệt hiệu suất.

Lưu ý rằng Hiệu ứng chuyển đổi CSS và Ảnh động CSS 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 ảnh động cơ bản. Nhưng nếu bạn cần làm điều gì đó thú vị, AnimationWorklet sẽ giúp bạn!