Làm mờ là một cách hiệu quả để chuyển hướng sự tập trung của người dùng. Việc làm mờ một số phần tử trực quan trong khi vẫn giữ các phần tử khác ở trạng thái rõ nét sẽ tự nhiên hướng sự tập trung của người dùng. Người dùng bỏ qua nội dung bị làm mờ và thay vào đó tập trung vào nội dung mà 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 bạn di chuột lên. 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
Làm mờ bằng ảnh động không phải là một lựa chọn hay vì 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 mờ dần giữa các phiên bản đó. Đồng nghiệp của tôi là Yi Gu đã viết một thư viện để xử lý mọi việc 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ó bất kỳ khoảng thời gian chuyển đổi nào. Việc tạo hiệu ứng làm mờ (chuyển từ trạng thái không bị mờ sang trạng thái bị mờ) có vẻ 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, có thể bạn nhận thấy rằng các hiệu ứng chuyển động không mượt mà chút nào, như bản minh hoạ này cho thấy nếu bạn không có một thiết bị mạnh mẽ. Chúng ta có thể làm tốt hơn không?
Vấn đề
Hiện tại, chúng ta không thể tạo hiệu ứng làm mờ một cách hiệu quả. Tuy nhiên, chúng ta có thể tìm ra một giải pháp đủ tốt, nhưng về mặt kỹ thuật, đây không phải là hiệu ứng làm mờ động. Để bắt đầu, trước tiên, hãy tìm hiểu lý do khiến hiệu ứng làm mờ có chuyển động bị chậm. Có 2 kỹ thuật làm mờ các phần tử trên web: Thuộc tính CSS filter và bộ lọc SVG. Nhờ khả năng hỗ trợ tăng lên và dễ sử dụng, các bộ lọc CSS thường được 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 làm mờ hoạt ảnh hoạt động với cả hai kỹ thuật. Vì vậy, hãy thử tìm điểm tắc nghẽn bằng cách xem xét Công cụ cho nhà phát triển.
Nếu bật tính năng "Đánh dấu nhấp nháy" trong Công cụ cho nhà phát triển, bạn sẽ không thấy bất kỳ điểm nhấp nháy nào. Có vẻ như không có hoạt động vẽ lại nào đang diễn ra. Và điều đó là chính xác về mặt kỹ thuật 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 đề xuất. Bất cứ khi nào một phần tử vừa được đề xuất vừa bị làm mờ, hiệu ứng làm mờ sẽ được GPU áp dụng bằng cách sử dụng một 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, một số pixel đầu vào phải được xem xét. 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à đó là vấn đề, chúng ta đang chạy một thao tác GPU khá tốn kém cho mỗi khung hình, vượt quá ngân sách khung hình 16 mili giây và do đó kết thúc ở mức thấp hơn 60 khung hình/giây.
Đi sâu vào vấn đề
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ể dùng thủ thuật đánh lừa thị giác! Thay vì tạo ảnh động cho giá trị làm mờ thực tế (bán kính của hiệu ứng làm mờ), chúng ta tính toán trước một vài bản sao bị làm mờ, trong đó giá trị làm mờ tăng theo cấp số nhân, sau đó chuyển đổi mờ giữa các bản sao đó bằng cách sử dụng opacity.
Hiệu ứng mờ dần là một chuỗi các hiệu ứng mờ dần xuất hiện và biến mất có độ mờ chồng chéo. 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 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ờ giai đoạn thứ tư (phiên bản cuối cùng). Trong trường hợp này, mỗi giai đoạn sẽ chiếm ¼ 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 cấp số nhân cho mỗi giai đoạn mang lại kết quả trực quan 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 bị làm mờ này vào lớp riêng của chúng (gọi là "quảng bá") bằng cách sử dụ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 sẽ cho phép chúng ta tải trước công việc làm mờ tốn kém. Hoá ra, logic này có sai só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 mờ thực sự tệ hơn so với trước đây.
Xem nhanh DevTools cho thấy GPU vẫn cực kỳ bậ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 tôi không thay đổi giá trị làm mờ nữa, chỉ thay đổi độ mờ. Vậy chuyện gì đang xảy ra? Vấn đề một lần nữa 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 chuyển đổi vừa bị làm mờ, thì hiệu ứng sẽ được GPU áp dụng. Vì vậy, mặc dù chúng ta không còn tạo hiệu ứ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 thậm chí 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 việc phải làm hơn trước, vì hầu hết thời gian đều có 2 hoạ tiết hiển thị cần được làm mờ độc lập.
Những gì chúng tôi nghĩ ra không đẹp mắt, nhưng nó giúp ảnh động chạy cực nhanh. 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 đó quảng bá một trình bao bọc mẹ. Nếu một phần tử vừa bị làm mờ vừa được làm nổi bật, thì GPU sẽ áp dụng hiệu ứng này. Đây là nguyên nhân khiến bản minh hoạ của chúng tôi chạy chậm. Nếu phần tử bị làm mờ nhưng không được tăng cường, thì độ mờ sẽ được chuyển đổi thành raster cho hoạ tiết mẹ gần nhất. Trong trường hợp này, đó là phần tử bao bọc mẹ được đề xuất. Giờ đây, hình ảnh bị làm mờ là hoạ tiết của phần tử mẹ và có thể được dùng lại cho tất cả các khung hình trong tương lai. Điều này chỉ hiệu quả vì chúng ta biết rằng các phần tử bị làm mờ không có hiệu ứng động và việc lưu vào bộ nhớ đệm thực sự có lợi. Sau đây là một bản minh hoạ triển khai kỹ thuật này. Không biết Moto G4 nghĩ gì về cách tiếp cận này? Cảnh báo: Tiết lộ nội dung!
Giờ đây, chúng ta có nhiều khoảng trống trên GPU và tốc độ 60 khung hình/giây mượt mà. Chúng tôi đã 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 nội dung cần làm mờ ở các mức độ khác nhau. Bạn có thể thắc mắc về cách hoạt động của tính năng này trong môi trường sản xuất vì tính năng này có thể gây ra một số tác dụng phụ không mong muốn với kiểu CSS của tác giả hoặc thậm chí cả JavaScript của họ. Bạn đã trả lời đúng. Hãy khám phá Shadow DOM!
Mặc dù hầu hết mọi người đều nghĩ về Shadow DOM như một cách để đính kèm các phần tử "nội bộ" vào Custom Elements, nhưng đây cũng là một nguyên tắc cơ bản về khả năng cô lập và hiệu suất! JavaScript và CSS không thể xuyên qua ranh giới Shadow DOM, cho phép chúng tôi sao chép nội dung mà không ảnh hưởng đến 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 để chuyển đổi thành raster và hiện sử dụng các <div> này làm thành phần lưu trữ bóng. Chúng ta tạo một ShadowRoot bằng cách sử dụ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 cũng phải sao chép tất cả các biểu định 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 phiên bản 1 và đối với những trình duyệt đó, chúng tôi chỉ sao chép nội dung và hy vọng không có gì bị hỏng. Chúng tôi có thể sử dụng Shadow DOM polyfill với ShadyCSS, nhưng chúng tôi không triển khai tính năng này trong thư viện của mình.
Vậy là xong. Sau hành trình khám phá quy trình kết xuất của Chrome, chúng ta đã tìm ra cách để tạo hiệu ứng làm mờ 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 hiệu ứng này một cách tuỳ tiện. Do thực tế là chúng tôi sao chép các phần tử DOM và buộc chúng vào lớp riêng, nên chúng tôi có thể đẩy giới hạn của các thiết bị cấp thấp. Việc sao chép tất cả biểu định kiểu vào mỗi ShadowRoot cũng là một nguy cơ tiềm ẩn về hiệu suất. Vì vậy, bạn nên quyết định xem mình 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. Nhưng đô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ủa chúng tôi cũng như bản minh hoạ và liên hệ với tôi trên Twitter nếu bạn có bất kỳ câu hỏi nào!