Cách Javascript hoạt động P13: Bên trong CSS & JS animation & các giải pháp tối ưu hiệu năng của nó

November 25, 2018 (5y ago)

Chào các bạn đến với bài thứ 13 trong series đục khoét và khám phá Javascript cũng như các thành phần của nó. Trong quá trình xác định và tìm hiểu các thành phần cốt lõi, tác giả cũng chia sẻ một số nguyên tắc mà họ đang dùng để xây dựng SessionStack, một ứng dụng Javascript hướng đến sự mạnh mẽ, hiệu năng cao và ổn định.

Khái quát

Mọi người đã biết rõ animation đóng vai trò cần thiết trong việc tạo ra các webapp hấp dẫn. Khi người dùng dần dần chuyển sự chú ý của họ sang UX và các doanh nghiệp bắt đầu nhận ra sự quan trọng của trải nghiệm người dùng thú vị & hoàn hảo thì các ứng dụng web dần trở nên nặng nề hơn & cần thể hiện nhiều UI động hơn. Tất cả những thứ này cần đến các animation phức tạp để tạo ra các chuyển dịch trạng thái mượt mà xuyên suốt hành trình trải nghiệm của người dùng. Giờ đây thì điều đó không còn là thứ gì đặc biệt nữa. User càng lúc càng giỏi và họ mong đợi ở những giao diện người dùng có tính tương tác và phản hồi cao.

Tuy nhiên animating giao diện không nhất thiết phải đơn giản, minh bạch. Thứ gì cần được animated, khi nào và cảm giác mà một animation mang lại, đó mới là những câu hỏi khó trả lời.

So sánh Javascript & CSS animation

Có 2 phương pháp chính để tạo web animation là dùng Javascript hoặc CSS. Không có lựa chọn nào đúng hay sai cả, tất cả tùy thuộc vào thứ mà bạn muốn đạt được.

Animate với CSS

Animating với CSS là cách đơn giản nhất để làm một thứ gì đó chuyển động trên màn hình.

Chúng ta sẽ bắt đầu với một ví dụ nhanh về việc di chuyển một phần tử 50px theo cả 2 trục X và Y. Có thể thực hiện bằng cách đặt một CSS transition với thời gian 1000ms.

.box {
  -webkit-transform: translate(0, 0);
  -webkit-transition: -webkit-transform 1000ms;

  transform: translate(0, 0);
  transition: transform 1000ms;
}

.box.move {
  -webkit-transform: translate(50px, 50px);
  transform: translate(50px, 50px);
}

Khi class move được thêm vào, giá trị transform bị thay đổi và transition bắt đầu.

Bên cạnh transition duration (thời gian dịch chuyển), có những tùy chọn cho easing, về bản chất thì đây là cảm giác của animation. Chúng ta sẽ tìm hiểu easing chi tiết hơn trong các phần sau của bài viết này.

Nếu như trong đoạn code trên, bạn tạo ra những class CSS riêng biệt để quản lý animation thì bạn có thể dùng Javascript để tắt/mở mỗi animation.

Nếu như bạn có phần tử sau:

<div class="box">Sample content.</div>

Bạn có thể dùng Javascript để tắt/mở animation của nó:

var boxElements = document.getElementsByClassName('box'),
  boxElementsLength = boxElements.length,
  i;

for (i = 0; i < boxElementsLength; i++) {
  boxElements[i].classList.add('move');
}

Đoạn code trên lấy tất cả những phần tử có class box và thêm class move vào để trigger animation.

Làm như thế này tạo sự cân bằng cho app của bạn. Bạn có thể tập trung vào quản lý trạng thái với Javascript và chỉ cần đơn giản đặt những class thích hợp vào phần tử cần phải đặt, để cho trình duyệt xử lý các animation. Nếu bạn tiếp tục tìm hiểu theo hướng này, bạn có thể listen sự kiện transitioned trên mỗi phần tử, nhưng chỉ nếu như bạn phải support cho mấy phiên bản cũ của IE:

Listen sự kiện transitioned (được bắn ra tại cuối thời điểm transition) giống như thế này:

var boxElement = document.querySelector('.box'); // Lấy phần tử đầu tiên có class `box`
boxElement.addEventListener('transitionend', onTransitionEnd, false);

function onTransitionEnd() {
  // Xử lý sự kiện transition đã hoàn thành.
}

Thêm nữa, khi sử dụng CSS transition, bạn có thể dùng CSS animation, nó cho phép bạn có quyền nhiều hơn để kiểm soát mỗi animation keyframe, duration và iteration.

Keyframes được dùng để hướng dẫn chỉ định trình duyệt những giá trị của thuộc tính CSS cần phải có tại mỗi thời điểm nhất định và nó sẽ giúp xử lý phần còn lại.

Ví dụ:

/**
 * Đây là phiên bản đơn giản, không có
 * tiền tố vendor. Nếu có thêm chúng thì
 * sẽ còn dài dòng hơn nữa.
 */
.box {
  /* Chọn animation */
  animation-name: movingBox;

  /* duration của animation */
  animation-duration: 2300ms;

  /* số lần mà ta muốn animation chạy */
  animation-iteration-count: infinite;

  /* làm cho animation đảo ngược vào mỗi vòng lặp lẻ */
  animation-direction: alternate;
}

@keyframes movingBox {
  0% {
    transform: translate(0, 0);
    opacity: 0.4;
  }

  25% {
    opacity: 0.9;
  }

  50% {
    transform: translate(150px, 200px);
    opacity: 0.2;
  }

  100% {
    transform: translate(40px, 30px);
    opacity: 0.8;
  }
}

Animation trông như thế này đây: https://sessionstack.github.io/blog/demos/keyframes/

Với CSS animation, bạn định nghĩa chính animation độc lập với phần tử đích và sử dụng thuộc tính animation-name để chọn animation được yêu cầu.

Các CSS animation phần nào được đặt sẵn tiền tố vender, với -webkit- đang được sử dụng trong Safari, Safari Mobile và Android. Chrome, Opera, Internet Explorer và Firefox tất cả đều không có sẵn tiền tố. Nhiều công cụ có thể giúp bạn thêm phiên bản tiền tố của CSS mà bạn cần, cho phép bạn viết code mà không cần phải thêm tiền tố.

Animate với Javascript

Tạo animation với Javascript phức tạp hơn nhiều so với sử dụng CSS transition hay animation nhưng thường thì nó cung cấp cho developer nhiều sức mạnh đáng kể.

Javascript animation được viết nội tuyến như là 1 phần của code. Bạn cũng có thể đóng gói nó bên trong các object. Bên dưới là code Javascript mà bạn cần để viết lại đoạn CSS transition ở phần trên:

var boxElement = document.querySelector('.box');
var animation = boxElement.animate(
  [{ transform: 'translate(0)' }, { transform: 'translate(150px, 200px)' }],
  500
);
animation.addEventListener('finish', function () {
  boxElement.style.transform = 'translate(150px, 200px)';
});

Mặc định thì Web Animation chỉ chỉnh sửa phần trình bày của một phần tử. Nếu như bạn muốn object của mình vẫn giữ nguyên vị trí lúc nó được di chuyển tới thì bạn nên sửa lại style của nó khi animation kết thúc. Đây là lý do tại sao chúng ta listen sự kiện finish trong code trên, đặt thuộc tính box.style.transform bằng giá trị translate(150px, 200px) thì nó sẽ thể hiện giống như trường hợp transform thứ 2 trong CSS animation ở trên.

Với Javascript animation, bạn hoàn toàn kiểm soát style của phần tử tại thời điểm bất kỳ. Nghĩa là bạn có thể làm animation chậm lại, tạm ngưng, dừng hẳn, đảo ngược và điều khiển phần tử cho tới khi phù hợp. Điều này cực kỳ hữu ích nếu bạn muốn xây dựng các ứng dụng hướng đối tượng phức tạp bởi vì bạn có thể đóng gói các hành vi của app một cách chính xác.

Easing là gì?

Chuyển động tự nhiên làm cho user cảm thấy dễ chịu hơn với webapp của bạn và mang đến trải nghiệm UX tốt hơn.

Một cách tự nhiên, không có thứ gì di chuyển theo đường thẳng từ điểm này đến điểm khác. Thật ra thì mọi thứ đều có xu hướng tăng tốc và giảm tốc khi chúng di chuyển trong thế giới vật lý của chúng ta và có rất nhiều yếu tố khác nhau ảnh hưởng. Bộ não con người được thiết kế bẩm sinh để cảm nhận những chuyển động nên khi bạn thực hiện animation trên webapp hãy nhớ những kiến thức này.

Một vài thuật ngữ bạn cần hiểu:

Easing cho phép bạn tạo các animation cho cảm giác tự nhiên hơn.

Các từ khóa cho easing

CSS transition và animation cho phép bạn chọn loại easing mà bạn muốn. Có nhiều từ khóa khác nhau ảnh hưởng đến easing của animation. Bạn cũng có thể tạo ra easing hoàn toàn của riêng bạn.

Dưới đây là 1 số từ khóa bạn có thể dùng trong CSS để điều khiển easing:

Giờ thì cùng tìm hiểu về chúng nào.

Linear animation

Các animation không có bất cứ kiểu easing nào thì được gọi là linear (thẳng hàng, còn hàn lâm hơn thì "Tuyến tính"). Đồ thị thể hiện một linear animation:

Thời gian càng tăng thì giá trị cũng tăng với lượng tương ứng. Với chuyển động linear, mọi thứ có xu hướng thiếu tự nhiên. Nói chung bạn nên tránh sử dụng chuyển động linear.

Đây là cách triển khai một linear animation đơn giản:

transition: transform 500ms linear;

Ease-out animation

Như đã nói ở trên, ease out làm cho animation bắt đầu nhanh hơn so với kiểu linear trong khi đó nó lại chậm dần khi kết thúc. Đồ thị trông như thế này:

Nhìn chung, ease out là sự lựa chọn tốt nhất cho UI bởi vì khởi động nhanh làm cho animation của bạn có cảm giác phản hồi tốt trong khi chậm dần khi kết thúc mang lại cảm giác tự nhiên do sự chuyển động không đồng nhất.

Có nhiều cách để triển khai hiệu ứng ease out nhưng cách đơn giản nhất là từ khóa ease-out trong CSS:

transition: transform 500ms ease-out;

Ease-in animation

Đây là kiểu đối lập lại với ease-out animation: bắt đầu chậm chạp và kết thúc nhanh. Đồ thị mô tả:

So với ease-out animation thì ease-in có hơi không bình thường vì chúng tạo ra cảm giác thiếu sự phản hồi, bởi vì chúng khởi động chậm. Kết thúc nhanh có thể cũng tạo ra cảm giác lạ tương tự, toàn bộ animation đều tăng tốc trong khi đối tượng ở thế giới thực có xu hướng giảm tốc khi dừng lại đột ngột.

Để dùng ease-in animation thì bạn có thể dùng từ khóa tương tự như trên:

transition: transform 500ms ease-in;

Ease-in-out animation

Đây là animation kết hợp của ease-in và ease-out. Đồ thị:

Không nên sử dụng anmation-duration quá lâu bởi vì nó mang lại cảm giác rằng UI của bạn thiếu sự phản hồi.

Cách sử dụng ease-in-out animation:

transition: transform 500ms ease-in-out;

Tùy biến easing

Bạn có thể định nghĩa đường cung easing cho riêng bạn để có thể kiểm soát nhiều hơn những cảm giác mà animation có thể tạo ra.

Trên thực tế, các từ khóa ease-in, ease-out, liner, ease được nối với những đường cung Bezier (Bézier curves) đã định nghĩa sẵn, bạn có thể xem chi tiết ở Thông số kỹ thuật của CSS transition hoặc của Web Animation

Đường cung Bezier

Cùng tìm hiểu về cách hoạt động của đường cung Bezier nào. Một cung Bezier nhận 4 giá trị, hoặc nói rõ hơn thì nó nhận vào 2 cặp số. Mỗi cặp định nghĩa tọa độ X và Y của 1 điểm kiểm soát thuộc khối cung Bezier. Điểm bắt đầu của cung Bezier có tọa độ (0, 0) và kết thúc là ở (1, 1). Bạn có thể đặt cả 2 cặp số. Giá trị X cho 2 kiểm kiểm soát phải nằm trong khoảng [0, 1] còn giá trị của Y thì có thể vượt quá giới hạn [0, 1], mặc dù thông số kỹ thuật không nói rõ là bao nhiêu.

Kể cả những thay đổi nhỏ trong giá trị X và Y của mỗi điểm kiểm soát mang đến cho bạn một cung hoàn toàn khác biệt. Ở 2 đồ thị bên dưới, cung Bezier có các điểm gần nhau nhưng khác tọa độ.

Như bạn thấy thì đồ thị khá là khác nhau. Điểm kiểm soát đầu tiên có vector với giá trị sai khác là (0.045, 0.183) còn điểm thứ 2 là (-0.427, -0.054)

Còn đây là phần CSS cho đường cung thứ 2:

transition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);

2 số đầu tiên là tọa độ X và Y của điểm kiểm soát thứ nhất, 2 số tiếp theo là của điểm kiểm soát thứ 2.

Tối ưu hóa hiệu năng

Bạn cần phải duy trì 60 khung hình/giây (60 fps - game thủ hiểu cái này lắm nè :D) khi làm animation nếu không thì nó sẽ ảnh hưởng tiêu cực đến UX.

Và như 1 lẽ dĩ nhiên thì animation không hề miễn phí. Không phải là chuyện tiền nong, mà là hiệu năng. Animate một vài thuộc tính có thể không tốn kém như một số khác. Ví dụ: animate width và height của 1 phần tử sẽ thay đổi trạng thái hình học của nó và là nguyên nhân ảnh hưởng đến các phần tử khác trên trang bị thay đổi vị trí hoặc kích thước. Quá trình này được gọi là layout. Chúng ta cũng đã thảo luận chi tiết về layout và rendering trong bài trước rồi.

Nói chung, bạn cần tránh animate những thuộc tính nào có thể trigger quá trình layout hoặc tô màu (paint). Với đa số các trình duyệt hiện đại thì điều này nghĩa là hạn chế animation với opacity và transform

Will-change

Bạn có thể dùng will-change để thông báo cho trình duyệt biết bạn có ý định thay đổi thuộc tính của 1 phần tử. Nó cho phép trình duyệt cài đặt sẵn sàng trước những tối hưu hóa thích hợp nhất khi bạn cần thay đổi. Tuy nhiên cũng đừng lạm dụng will-change, bởi vì làm như thế có thể làm cho trình duyệt hao tốn tài nguyên và quay ngược lại gây ra nhiều vấn đề hơn về hiệu năng.

Cách để thêm will-change cho transform và opacity:

.box {
  will-change: transform, opacity;
}

Trình duyệt hỗ trợ:

Chọn Javascript hay CSS

Có lẽ bạn cũng đã thấy rằng không có câu trả lời đúng cho câu hỏi này, tùy thuộc vào nhiều yếu tố, dưới đây là những thứ bạn cần cân nhắc mỗi khi lựa chọn:

Chọn đúng thứ để animate

Những animation tuyệt vời thường tạo nên sự thích thú và gắn kết giữa project của bạn với người dùng. Bạn có thể animate bất cứ thứ gì bạn muốn, dù là width, height, position, color, background, vân vân, nhưng cần phải chú ý đến những nút thắt cổ chai tiềm tàng về hiệu năng. Những animation được chọn lựa 1 cách bất cẩn có thể ảnh hưởng tiêu cực đến UX, vì vậy animation cần phải vừa phù hợp vừa tốt cho hiệu năng. Animate càng ít càng tốt, chỉ nên animate khi bạn cần UX trở nên tự nhiên hơn nhưng đừng lạm dụng nó.

Dùng animation để hỗ trợ tương tác

Không nên animate chỉ vì bạn thích thế. Thay vì vậy, sử dụng animation tại những vị trí chiến lược để củng cố thêm về tương tác người dùng. Tránh animation làm ngắt quãng hoặc cản trở một cách không cần thiết các hoạt động của user

Tránh animate những thuộc tính phức tạp

Chỉ có duy nhất 1 thứ tệ hơn cả animation đặt sai chỗ chính là những animation làm cho trang web bị lag. Kiểu animation này làm cho user cảm thấy bực bội và chán nản.

Team tác giả sử dụng animation khá dễ dàng với SessionStack. Nói chung, họ theo sát những nguyên tắc đã nêu ở trên nhưng cũng có một số trường hợp mà họ phải dùng animation vì sự phức tạp của UI. SessionStack phải tái tạo lại dưới dạng video toàn bộ những thứ xảy ra với người dùng cuối tại thời điểm họ gặp phải vấn đề khi đang lướt web hay dùng webapp. Để làm được điều này SessionStack tận dụng duy nhất những dữ liệu thu thập được xuyên suốt phiên làm việc: sự kiện từ user, thay đổi trên DOM, request mạng, biệt lệ, thông báo debug, vân vân. Trình chơi của họ được tối ưu hóa khá tốt để có thể render một cách chính xác và sử dụng toàn bộ dữ liệu thu thập được để giả lập chính xác đến từng pixel trình duyệt của người dùng và những thứ xảy ra trên nó, cả về khía cạnh nhìn thấy được và góc nhìn kỹ thuật.

Để đảm bảo quá trình tái tạo diễn ra một cách tự nhiên, đặc biệt với những phiên làm việc kéo dài và nặng dữ liệu, team tác giả sử dụng các animation để chỉ định chính xác hoạt động loading/buffering và bám sát các nguyên tắc tốt nhất để triển khai chúng, do đó họ không cần quá nhiều CPU và vẫn có thể để cho event loop được rảnh tay render các session.