Giới thiệu
Các trang web có hiệu ứng thị giác Parallax đang rất thịnh hành gần đây, hãy xem những trang web sau:
- Old Pulteney Row to the Pole
- Adidas Snowboarding
- BBC News – James Bond: Ô tô, câu cửa miệng và nụ hôn
Nếu bạn chưa quen, đó là những trang web có cấu trúc hình ảnh của trang thay đổi khi bạn cuộn. Thông thường, các phần tử trong tỷ lệ trang, xoay hoặc di chuyển theo tỷ lệ với vị trí cuộn trên trang.
Việc bạn có thích các trang web có hiệu ứng thị giác 3D hay không là một chuyện, nhưng điều bạn có thể nói một cách khá tự tin là những trang web đó là một hố đen về hiệu suất. Lý do là các trình duyệt thường được tối ưu hoá cho trường hợp nội dung mới xuất hiện ở đầu hoặc cuối màn hình khi bạn cuộn (tuỳ thuộc vào hướng cuộn của bạn). Nói chung, trình duyệt hoạt động tốt nhất khi có rất ít thay đổi về mặt hình ảnh trong quá trình cuộn. Đối với trang web có hiệu ứng thị giác 3D, điều này hiếm khi xảy ra vì nhiều khi các thành phần hình ảnh lớn trên trang thay đổi, khiến trình duyệt phải vẽ lại toàn bộ trang.
Bạn có thể khái quát một trang web có hiệu ứng thị sai như sau:
- Các phần tử nền khi bạn cuộn lên và xuống, thay đổi vị trí, độ xoay và tỷ lệ.
- Nội dung trang, chẳng hạn như văn bản hoặc hình ảnh nhỏ hơn, cuộn theo kiểu từ trên xuống dưới thông thường.
Trước đây, chúng tôi đã đề cập đến hiệu suất cuộn và các cách bạn có thể cải thiện khả năng phản hồi của ứng dụng. Bài viết này được xây dựng dựa trên nền tảng đó, vì vậy, bạn nên đọc bài viết này nếu chưa đọc.
Vậy câu hỏi đặt ra là nếu bạn đang xây dựng một trang web có hiệu ứng cuộn song song, bạn có bị buộc phải vẽ lại tốn kém hay có phương pháp thay thế nào để bạn có thể tối đa hoá hiệu suất không? Hãy cùng xem xét các lựa chọn của chúng ta.
Cách 1: Sử dụng phần tử DOM và vị trí tuyệt đối
Đây có vẻ là phương pháp mặc định mà hầu hết mọi người sử dụng. Có một loạt phần tử trong trang và mỗi khi một sự kiện cuộn được kích hoạt, một loạt nội dung cập nhật hình ảnh sẽ được thực hiện để biến đổi các phần tử đó.
Nếu khởi động Dòng thời gian của DevTools ở chế độ khung và cuộn xung quanh, bạn sẽ nhận thấy có các thao tác vẽ toàn màn hình tốn kém. Nếu cuộn nhiều, bạn có thể thấy một số sự kiện cuộn bên trong một khung, mỗi sự kiện sẽ kích hoạt công việc bố cục.
Điều quan trọng cần lưu ý là để đạt được tốc độ 60 khung hình/giây (tương ứng với tốc độ làm mới màn hình thông thường là 60 Hz), chúng ta chỉ có hơn 16 mili giây để hoàn tất mọi việc. Trong phiên bản đầu tiên này, chúng ta sẽ thực hiện các bản cập nhật hình ảnh mỗi khi nhận được một sự kiện cuộn. Tuy nhiên, như đã thảo luận trong các bài viết trước về ảnh động gọn nhẹ hơn, hiệu quả hơn với requestAnimationFrame và hiệu suất cuộn, việc này không trùng khớp với lịch cập nhật của trình duyệt, vì vậy, chúng ta có thể bỏ lỡ các khung hình hoặc làm quá nhiều việc bên trong mỗi khung hình. Điều đó có thể dễ dàng khiến trang web của bạn có cảm giác giật và không tự nhiên, khiến người dùng thất vọng và mèo con không vui.
Hãy di chuyển mã cập nhật từ sự kiện cuộn sang lệnh gọi lại requestAnimationFrame
và chỉ cần ghi lại giá trị cuộn trong lệnh gọi lại của sự kiện cuộn.
Nếu lặp lại kiểm thử cuộn, bạn có thể nhận thấy sự cải thiện nhỏ, mặc dù không đáng kể. Lý do là thao tác bố cục mà chúng ta kích hoạt bằng cách cuộn không phải là quá tốn kém, nhưng trong các trường hợp sử dụng khác, thao tác này thực sự có thể tốn kém. Hiện tại, ít nhất chúng ta chỉ thực hiện một thao tác bố cục trong mỗi khung.
Giờ đây, chúng ta có thể xử lý một hoặc một trăm sự kiện cuộn trên mỗi khung hình, nhưng quan trọng là chúng ta chỉ lưu trữ giá trị gần đây nhất để sử dụng bất cứ khi nào lệnh gọi lại requestAnimationFrame
chạy và thực hiện các bản cập nhật hình ảnh. Điểm mấu chốt là bạn đã chuyển từ việc cố gắng buộc cập nhật hình ảnh mỗi khi nhận được sự kiện cuộn sang yêu cầu trình duyệt cung cấp cho bạn một cửa sổ thích hợp để thực hiện việc này. Bạn thật dễ thương.
Vấn đề chính với phương pháp này, requestAnimationFrame
hay không, là về cơ bản chúng ta có một lớp cho toàn bộ trang và bằng cách di chuyển các thành phần hình ảnh này xung quanh, chúng ta cần phải vẽ lại nhiều (và tốn kém). Thông thường, việc vẽ là một thao tác chặn (mặc dù điều đó đang thay đổi), nghĩa là trình duyệt không thể làm bất kỳ công việc nào khác và chúng ta thường chạy vượt quá ngân sách khung hình là 16 mili giây và mọi thứ vẫn bị giật.
Cách 2: Sử dụng các phần tử DOM và phép biến đổi 3D
Thay vì sử dụng các vị trí tuyệt đối, chúng ta có thể áp dụng một phương pháp khác là áp dụng phép biến đổi 3D cho các phần tử. Trong trường hợp này, chúng ta thấy các phần tử có phép biến đổi 3D được áp dụng sẽ được cấp một lớp mới cho mỗi phần tử và trong trình duyệt WebKit, điều này thường cũng khiến chuyển sang trình kết hợp phần cứng. Ngược lại, trong Tuỳ chọn 1, chúng ta có một lớp lớn cho trang cần được vẽ lại khi có bất kỳ thay đổi nào và tất cả các hoạt động vẽ và kết hợp đều do CPU xử lý.
Điều đó có nghĩa là với tuỳ chọn này, mọi thứ sẽ khác: chúng ta có thể có một lớp cho bất kỳ phần tử nào mà chúng ta áp dụng phép biến đổi 3D. Nếu tất cả những gì chúng ta làm từ thời điểm này là thực hiện thêm các phép biến đổi trên các phần tử, thì chúng ta sẽ không cần phải vẽ lại lớp. GPU có thể xử lý việc di chuyển các phần tử xung quanh và kết hợp trang cuối cùng với nhau.
Nhiều khi mọi người chỉ sử dụng bản hack -webkit-transform: translateZ(0);
và thấy hiệu suất cải thiện một cách kỳ diệu. Mặc dù cách này vẫn hoạt động cho đến ngày nay, nhưng vẫn có một số vấn đề:
- Không tương thích trên nhiều trình duyệt.
- Phương thức này buộc trình duyệt phải tạo một lớp mới cho mọi phần tử đã chuyển đổi. Nhiều lớp có thể gây ra các nút thắt cổ chai hiệu suất khác, vì vậy, hãy sử dụng một cách tiết kiệm!
- Tính năng này đã bị tắt đối với một số cổng WebKit (điểm đầu tiên từ dưới lên!).
Nếu bạn chọn chuyển đổi sang 3D, hãy thận trọng vì đây chỉ là giải pháp tạm thời cho vấn đề của bạn! Lý tưởng nhất là chúng ta sẽ thấy các đặc điểm kết xuất tương tự từ các phép biến đổi 2D như chúng ta làm với 3D. Các trình duyệt đang phát triển với tốc độ đáng kinh ngạc, vì vậy, hy vọng chúng ta sẽ thấy điều đó trước khi điều đó xảy ra.
Cuối cùng, bạn nên cố gắng tránh vẽ bất cứ khi nào có thể và chỉ cần di chuyển các phần tử hiện có xung quanh trang. Ví dụ: đây là phương pháp điển hình trong các trang web có hiệu ứng thị sai, sử dụng các div có chiều cao cố định và thay đổi vị trí nền để tạo hiệu ứng. Rất tiếc, điều đó có nghĩa là phần tử cần được vẽ lại trên mỗi lượt truyền, điều này có thể làm giảm hiệu suất. Thay vào đó, nếu có thể, bạn nên tạo phần tử (gói phần tử đó bên trong một div bằng overflow: hidden
nếu cần) và chỉ cần dịch phần tử đó.
Cách 3: Sử dụng canvas có vị trí cố định hoặc WebGL
Lựa chọn cuối cùng mà chúng ta sẽ xem xét là sử dụng canvas có vị trí cố định ở cuối trang để vẽ hình ảnh đã chuyển đổi. Thoạt nhìn, đây có vẻ không phải là giải pháp hiệu quả nhất, nhưng thực tế có một số lợi ích của phương pháp này:
- Chúng ta không cần nhiều công việc của trình tổng hợp nữa vì chỉ có một phần tử là canvas.
- Chúng ta đang xử lý hiệu quả một bitmap tăng tốc phần cứng.
- API Canvas2D rất phù hợp với loại phép biến đổi mà chúng ta muốn thực hiện, nghĩa là việc phát triển và bảo trì sẽ dễ quản lý hơn.
Khi sử dụng phần tử canvas, chúng ta sẽ có một lớp mới, nhưng đó chỉ là một lớp, trong khi ở Cách 2, chúng ta thực sự được cung cấp một lớp mới cho mọi phần tử có áp dụng phép biến đổi 3D, vì vậy, chúng ta có khối lượng công việc tăng lên khi kết hợp tất cả các lớp đó với nhau. Đây cũng là giải pháp tương thích nhất hiện nay khi xem xét các phương thức triển khai khác nhau của các phép biến đổi trên nhiều trình duyệt.
/**
* Updates and draws in the underlying visual elements to the canvas.
*/
function updateElements () {
var relativeY = lastScrollY / h;
// Fill the canvas up
context.fillStyle = "#1e2124";
context.fillRect(0, 0, canvas.width, canvas.height);
// Draw the background
context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));
// Draw each of the blobs in turn
context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));
// Allow another rAF call to be scheduled
ticking = false;
}
/**
* Calculates a relative disposition given the page's scroll
* range normalized from 0 to 1
* @param {number} base The starting value.
* @param {number} range The amount of pixels it can move.
* @param {number} relY The normalized scroll value.
* @param {number} offset A base normalized value from which to start the scroll behavior.
* @returns {number} The updated position value.
*/
function pos(base, range, relY, offset) {
return base + limit(0, 1, relY - offset) * range;
}
/**
* Clamps a number to a range.
* @param {number} min The minimum value.
* @param {number} max The maximum value.
* @param {number} value The value to limit.
* @returns {number} The clamped value.
*/
function limit(min, max, value) {
return Math.max(min, Math.min(max, value));
}
Phương pháp này thực sự hiệu quả khi bạn xử lý các hình ảnh lớn (hoặc các thành phần khác có thể dễ dàng được ghi vào canvas). Và chắc chắn việc xử lý các khối văn bản lớn sẽ khó khăn hơn, nhưng tuỳ thuộc vào trang web của bạn, đây có thể là giải pháp phù hợp nhất. Nếu phải xử lý văn bản trong canvas, bạn sẽ phải sử dụng phương thức API fillText
, nhưng điều này sẽ làm giảm khả năng hỗ trợ tiếp cận (bạn chỉ quét văn bản thành bitmap!) và giờ đây, bạn sẽ phải xử lý việc xuống dòng và một loạt các vấn đề khác. Nếu có thể, bạn nên tránh việc này và có thể sẽ được phục vụ tốt hơn bằng cách sử dụng phương pháp biến đổi ở trên.
Vì chúng ta đang thực hiện việc này ở mức độ tối đa có thể, nên không có lý do gì để giả định rằng hiệu ứng thị sai phải được thực hiện bên trong phần tử canvas. Nếu trình duyệt hỗ trợ, chúng ta có thể sử dụng WebGL. Điểm mấu chốt ở đây là WebGL có tuyến đường trực tiếp nhất trong tất cả các API đến thẻ đồ hoạ và do đó, đây là ứng cử viên có nhiều khả năng nhất để đạt được 60 khung hình/giây, đặc biệt là nếu các hiệu ứng của trang web phức tạp.
Phản ứng tức thì của bạn có thể là WebGL quá mức hoặc không phổ biến về khả năng hỗ trợ, nhưng nếu sử dụng một công cụ như Three.js, thì bạn luôn có thể quay lại sử dụng phần tử canvas và mã của bạn được trừu tượng một cách nhất quán và thân thiện. Tất cả những gì chúng ta cần làm là sử dụng Modernizr để kiểm tra khả năng hỗ trợ API thích hợp:
// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
renderer = new THREE.CanvasRenderer();
}
Cuối cùng, về phương pháp này, nếu không muốn thêm các phần tử bổ sung vào trang, bạn luôn có thể sử dụng canvas làm phần tử nền trong cả Firefox và các trình duyệt dựa trên WebKit. Rõ ràng là điều này không phổ biến, vì vậy, như thường lệ, bạn nên thận trọng khi xử lý.
Quyền quyết định là ở bạn
Lý do chính khiến nhà phát triển mặc định sử dụng các phần tử được định vị tuyệt đối thay vì bất kỳ tuỳ chọn nào khác có thể chỉ đơn giản là khả năng hỗ trợ phổ biến. Điều này ở một mức độ nào đó là ảo tưởng, vì các trình duyệt cũ đang được nhắm đến có thể mang lại trải nghiệm hiển thị cực kỳ kém. Ngay cả trong các trình duyệt hiện đại ngày nay, việc sử dụng các phần tử được định vị tuyệt đối không nhất thiết phải mang lại hiệu suất tốt!
Chuyển đổi, chắc chắn là loại 3D, cho phép bạn làm việc trực tiếp với các phần tử DOM và đạt được tốc độ khung hình ổn định. Mấu chốt để thành công ở đây là tránh vẽ bất cứ khi nào có thể và chỉ cần thử di chuyển các phần tử xung quanh. Xin lưu ý rằng cách trình duyệt WebKit tạo lớp không nhất thiết phải tương quan với các công cụ trình duyệt khác. Vì vậy, hãy nhớ kiểm thử trước khi cam kết với giải pháp đó.
Nếu bạn chỉ nhắm đến các trình duyệt hàng đầu và có thể hiển thị trang web bằng canvas, thì đó có thể là lựa chọn tốt nhất cho bạn. Chắc chắn nếu sử dụng Three.js, bạn có thể dễ dàng hoán đổi và thay đổi giữa các trình kết xuất tuỳ thuộc vào khả năng hỗ trợ mà bạn yêu cầu.
Kết luận
Chúng tôi đã đánh giá một số phương pháp để xử lý các trang web có hiệu ứng thị giác 3D, từ các phần tử có vị trí tuyệt đối đến việc sử dụng canvas có vị trí cố định. Tất nhiên, cách triển khai mà bạn thực hiện sẽ phụ thuộc vào mục tiêu bạn đang cố gắng đạt được và thiết kế cụ thể mà bạn đang làm việc, nhưng bạn luôn có nhiều lựa chọn.
Và như thường lệ, bất kể bạn thử phương pháp nào, hãy kiểm thử chứ không phải đoán mò.