Tạo ảnh động cho hiệu ứng làm mờ

Làm mờ là một cách hiệu quả để chuyển hướng tiêu điểm của người dùng. Việc làm cho một số phần tử hình ảnh trông mờ trong khi vẫn giữ các phần tử khác ở tiêu điểm sẽ tự nhiên hướng sự chú ý của người dùng. Người dùng sẽ bỏ qua nội dung bị làm mờ và tập trung vào nội dung họ có thể đọc. Một ví dụ là danh sách các biểu tượng hiển thị thông tin chi tiết về từng mục khi di chuột qua. Trong thời gian đó, các lựa chọn còn lại có thể bị làm mờ để chuyển hướng người dùng đến thông tin mới hiển thị.

TL;DR

Bạn không nên tạo hiệu ứng mờ động vì hiệu ứng này rất chậm. Thay vào đó, hãy tính toán trước một loạt các phiên bản ngày càng mờ và chuyển đổi giữa các phiên bản đó. Đồng nghiệp của tôi, Yi Gu, đã viết một thư viện để xử lý mọi thứ cho bạn! Hãy xem bản minh hoạ của chúng tôi.

Tuy nhiên, kỹ thuật này có thể gây khó chịu khi được áp dụng mà không có giai đoạn chuyển đổi nào. Tạo hiệu ứng mờ cho ảnh động – chuyển đổi từ không mờ sang mờ – có vẻ như là một lựa chọn hợp lý, nhưng nếu từng thử làm việc này trên web, bạn có thể nhận thấy rằng ảnh động không hề mượt mà, như bản minh hoạ này cho thấy nếu bạn không có máy mạnh. Chúng ta có thể làm tốt hơn không?

Vấn đề

CPU sẽ chuyển đổi mã đánh dấu thành hoạ tiết. Kết cấu được tải lên GPU. GPU vẽ các hoạ tiết này vào vùng đệm khung hình bằng chương trình đổ bóng. Hiệu ứng làm mờ diễn ra trong chương trình đổ bóng.

Hiện tại, chúng ta không thể tạo hiệu ứng làm mờ ảnh động một cách hiệu quả. Tuy nhiên, chúng ta có thể tìm thấy một giải pháp đủ tốt, nhưng về mặt kỹ thuật, không phải là hiệu ứng làm mờ ảnh động. Để bắt đầu, trước tiên, hãy tìm hiểu lý do hiệu ứng làm mờ ảnh động bị chậm. Để làm mờ các phần tử trên web, có hai kỹ thuật: Thuộc tính filter CSS và bộ lọc SVG. Nhờ khả năng hỗ trợ và dễ sử dụng cao hơn, bộ lọc CSS thường được sử dụng. Rất tiếc, nếu bắt buộc phải hỗ trợ Internet Explorer, bạn không có lựa chọn nào khác ngoài việc sử dụng bộ lọc SVG vì IE 10 và 11 hỗ trợ các bộ lọc đó nhưng không hỗ trợ bộ lọc CSS. Tin vui là giải pháp của chúng tôi để tạo hiệu ứng mờ cho ảnh động hoạt động với cả hai kỹ thuật. Vì vậy, hãy thử tìm nút thắt cổ chai bằng cách xem DevTools.

Nếu bật tính năng "Paint Flashing" (Chớp sơn) trong DevTools, bạn sẽ không thấy bất kỳ ánh sáng chớp nào. Có vẻ như không có quá trình vẽ lại nào đang diễn ra. Và điều đó về mặt kỹ thuật là chính xác vì "vẽ lại" đề cập đến việc CPU phải vẽ lại hoạ tiết của một phần tử được quảng bá. Bất cứ khi nào một phần tử được quảng bá làm mờ, GPU sẽ áp dụng hiệu ứng làm mờ bằng chương trình đổ bóng.

Cả bộ lọc SVG và bộ lọc CSS đều sử dụng bộ lọc tích chập để áp dụng hiệu ứng làm mờ. Bộ lọc tích chập khá tốn kém vì đối với mỗi pixel đầu ra, bạn phải xem xét một số pixel đầu vào. Hình ảnh càng lớn hoặc bán kính làm mờ càng lớn thì hiệu ứng càng tốn kém.

Và đó chính là vấn đề, chúng ta đang chạy một thao tác GPU khá tốn kém trên mỗi khung hình, làm vượt quá ngân sách khung hình là 16 mili giây, do đó kết quả cuối cùng sẽ thấp hơn nhiều so với 60 khung hình/giây.

Chuột sa hố

Vậy chúng ta có thể làm gì để quá trình này diễn ra suôn sẻ? Chúng ta có thể sử dụng mánh khóe! Thay vì tạo ảnh động cho giá trị độ mờ thực tế (bán kính của độ mờ), chúng ta tính toán trước một vài bản sao bị mờ, trong đó giá trị độ mờ tăng theo cấp số nhân, sau đó chuyển đổi giữa các bản sao đó bằng opacity.

Hiệu ứng chuyển đổi là một loạt các hiệu ứng mờ dần và mờ dần chồng chéo nhau. Ví dụ: nếu có 4 giai đoạn làm mờ, chúng ta sẽ làm mờ giai đoạn đầu tiên trong khi làm mờ giai đoạn thứ hai cùng một lúc. Khi giai đoạn thứ hai đạt độ mờ 100% và giai đoạn đầu tiên đạt độ mờ 0%, chúng ta sẽ làm mờ giai đoạn thứ hai trong khi làm mờ giai đoạn thứ ba. Sau khi hoàn tất, cuối cùng chúng ta sẽ làm mờ giai đoạn thứ ba và làm mờ phiên bản thứ tư và cuối cùng. Trong trường hợp này, mỗi giai đoạn sẽ mất ¼ tổng thời lượng mong muốn. Về mặt hình ảnh, hiệu ứng này trông rất giống với hiệu ứng làm mờ động thực tế.

Trong các thử nghiệm của chúng tôi, việc tăng bán kính làm mờ theo hàm mũ cho mỗi giai đoạn đã mang lại kết quả hình ảnh tốt nhất. Ví dụ: Nếu có 4 giai đoạn làm mờ, chúng ta sẽ áp dụng filter: blur(2^n) cho từng giai đoạn, tức là giai đoạn 0: 1px, giai đoạn 1: 2px, giai đoạn 2: 4px và giai đoạn 3: 8px. Nếu chúng ta buộc mỗi bản sao mờ này vào lớp riêng (được gọi là "đẩy lên") bằng will-change: transform, thì việc thay đổi độ mờ trên các phần tử này sẽ cực kỳ nhanh. Về lý thuyết, điều này cho phép chúng ta tải trước công việc làm mờ tốn kém. Hóa ra, logic này có khiếm khuyết. Nếu chạy bản minh hoạ này, bạn sẽ thấy tốc độ khung hình vẫn dưới 60 khung hình/giây và hiện tượng làm mờ thực sự tệ hơn so với trước.

DevTools cho thấy một dấu vết trong đó GPU có thời gian bận kéo dài.

Một lượt xem nhanh vào DevTools cho thấy GPU vẫn cực kỳ bận rộn và kéo dài mỗi khung hình đến khoảng 90 mili giây. Nhưng tại sao? Chúng ta không thay đổi giá trị làm mờ nữa, chỉ thay đổi độ mờ, vậy điều gì đang xảy ra? Một lần nữa, vấn đề nằm ở bản chất của hiệu ứng làm mờ: Như đã giải thích trước đó, nếu phần tử vừa được quảng bá vừa được làm mờ, thì hiệu ứng này sẽ được GPU áp dụng. Vì vậy, mặc dù chúng ta không còn tạo ảnh động cho giá trị làm mờ nữa, nhưng bản thân hoạ tiết vẫn chưa được làm mờ và cần được GPU làm mờ lại mỗi khung hình. Lý do khiến tốc độ khung hình còn tệ hơn trước là do so với cách triển khai đơn giản, GPU thực sự có nhiều công việc hơn trước, vì hầu hết thời gian, hai hoạ tiết hiển thị cần được làm mờ độc lập.

Kết quả chúng ta có được không đẹp mắt nhưng giúp ảnh động chạy nhanh như chớp. Chúng ta quay lại việc không quảng bá phần tử cần làm mờ, mà thay vào đó là quảng bá trình bao bọc mẹ. Nếu một phần tử vừa được làm mờ vừa được quảng bá, thì hiệu ứng sẽ được GPU áp dụng. Đây là nguyên nhân khiến bản minh hoạ của chúng ta bị chậm. Nếu phần tử bị làm mờ nhưng không được quảng bá, thì hiệu ứng làm mờ sẽ được quét thành hoạ tiết mẹ gần nhất. Trong trường hợp của chúng ta, đó là phần tử trình bao bọc mẹ được quảng bá. Hình ảnh mờ hiện là hoạ tiết của phần tử mẹ và có thể được sử dụng lại cho tất cả các khung hình trong tương lai. Điều này chỉ hoạt động vì chúng ta biết rằng các phần tử được làm mờ không có ảnh động và việc lưu các phần tử đó vào bộ nhớ đệm thực sự có lợi. Dưới đây là một bản minh hoạ triển khai kỹ thuật này. Tôi tự hỏi Moto G4 nghĩ gì về phương pháp này? Cảnh báo tiết lộ nội dung: ứng dụng này cho rằng đó là một ý tưởng tuyệt vời:

DevTools cho thấy dấu vết GPU có nhiều thời gian rảnh.

Bây giờ, chúng ta có rất nhiều khoảng không trên GPU và tốc độ 60 khung hình/giây mượt mà. Chúng ta đã làm được!

Phát hành công khai

Trong bản minh hoạ, chúng tôi đã sao chép cấu trúc DOM nhiều lần để có các bản sao của nội dung làm mờ ở nhiều mức độ. Bạn có thể thắc mắc cách hoạt động của cách này trong môi trường sản xuất vì điều đó có thể gây ra một số hiệu ứng phụ ngoài mong muốn với các kiểu CSS của tác giả hoặc thậm chí là JavaScript của họ. Bạn nói đúng. Giới thiệu về Shadow DOM!

Mặc dù hầu hết mọi người đều nghĩ rằng Shadow DOM là một cách để đính kèm các phần tử "nội bộ" vào Thành phần tuỳ chỉnh, nhưng nó cũng là một nguyên tắc cô lập và hiệu suất! JavaScript và CSS không thể xuyên thủng các ranh giới của Shadow DOM, cho phép chúng ta sao chép nội dung mà không can thiệp vào các kiểu hoặc logic ứng dụng của nhà phát triển. Chúng ta đã có một phần tử <div> cho mỗi bản sao để quét và hiện sử dụng các <div> này làm máy chủ bóng. Chúng ta tạo một ShadowRoot bằng attachShadow({mode: 'closed'}) và đính kèm một bản sao của nội dung vào ShadowRoot thay vì chính <div>. Chúng ta phải đảm bảo rằng cũng sao chép tất cả các tệp kiểu vào ShadowRoot để đảm bảo rằng các bản sao của chúng ta được tạo kiểu giống như bản gốc.

Một số trình duyệt không hỗ trợ Shadow DOM v1. Đối với những trình duyệt đó, chúng ta chỉ cần sao chép nội dung và hy vọng không có vấn đề gì xảy ra. Chúng ta có thể sử dụng Shadow DOM polyfill với ShadyCSS, nhưng chúng ta không triển khai tính năng này trong thư viện.

Vậy là xong. Sau hành trình tìm hiểu quy trình kết xuất của Chrome, chúng tôi đã tìm ra cách tạo hiệu ứng làm mờ ảnh động một cách hiệu quả trên các trình duyệt!

Kết luận

Bạn không nên sử dụng loại hiệu ứng này một cách dễ dãi. Do chúng ta sao chép các phần tử DOM và buộc các phần tử đó vào lớp riêng, nên chúng ta có thể đẩy giới hạn của các thiết bị cấp thấp hơn. Việc sao chép tất cả các tệp kiểu vào mỗi ShadowRoot cũng là một rủi ro tiềm ẩn về hiệu suất. Vì vậy, bạn nên quyết định xem bạn muốn điều chỉnh logic và kiểu để không bị ảnh hưởng bởi các bản sao trong LightDOM hay sử dụng kỹ thuật ShadowDOM của chúng tôi. Tuy nhiên, đôi khi kỹ thuật của chúng tôi có thể là một khoản đầu tư đáng giá. Hãy xem mã trong kho lưu trữ GitHub cũng như mã minh hoạ và liên hệ với tôi trên Twitter nếu bạn có câu hỏi!