Quảng cáo thị sai'

Giới thiệu

Các trang web thị sai thịnh hành gần đây, hãy xem các trang sau:

Nếu bạn không quen thuộc với các trang này, thì đó là các 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 tương ứng với vị trí cuộn trên trang.

Một trang thị sai minh hoạ
Trang minh hoạ của chúng tôi hoàn thiện với hiệu ứng thị sai

Dù bạn có thích các trang web lừa đảo hay không là một chuyện, nhưng bạn có thể tự tin nói rằng chúng là một lỗ hổng hiệu suất. Lý do là trình duyệt có xu hướ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 khi cuộn. Đối với một trang web thị sai, điều này hiếm khi xảy ra vì nhiều lần các thành phần trực quan lớn trên trang thay đổi, khiến trình duyệt phải vẽ lại toàn bộ trang.

Bạn nên khái quát hoá một trang web lừa đảo như sau:

  • Các thành phần nền mà khi bạn cuộn lên và xuống sẽ thay đổi vị trí, xoay và tỷ lệ của chúng.
  • 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 gặp.

Trước đây chúng tôi đã đề cập đến hiệu suất cuộn và cách bạn có thể xem xét để 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 nếu bạn chưa thực hiện.

Vì vậy, câu hỏi là liệu bạn đang xây dựng một trang web cuộn thị sai, bạn có bị khóa với các lần vẽ lại tốn kém không hoặc có phương pháp thay thế nào bạn có thể thực hiện để tối đa hóa hiệu suất không? Hãy tìm hiểu các lựa chọn của chúng tôi.

Cách 1: Sử dụng các 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 đều áp dụng. Có rất nhiều 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 các nội dung cập nhật bằng hình ảnh đã được thực hiện để biến đổi chúng.

Nếu khởi động Dòng thời gian cho công cụ cho nhà phát triển ở chế độ khung và cuộn xung quanh, bạn sẽ thấy có các hoạt động vẽ toàn màn hình tốn kém. Ngoài ra, nếu cuộn nhiều, bạn có thể thấy một vài sự kiện cuộn bên trong một khung, mỗi sự kiện trong số đó sẽ kích hoạt công việc bố cục.

Công cụ của Chrome cho nhà phát triển không có sự kiện cuộn được gỡ bỏ.
DevTools cho thấy nội dung hiển thị lớn và nhiều bố cục do sự kiện kích hoạt trong một khung hình.

Điều quan trọng cần ghi nhớ là để đạt tốc độ 60 khung hình/giây (phù hợp với tốc độ làm mới màn hình thông thường là 60Hz), chúng ta chỉ có hơn 16 mili giây để hoàn thành mọi việc. Trong phiên bản đầu tiên này, chúng ta sẽ cập nhật hình ảnh mỗi khi có sự kiện cuộn, nhưng như chúng ta đã thảo luận trong các bài viết trước về ảnh động gọn gàng hơn với requestAnimationFramehiệu suất cuộn, điều này không trùng với lịch cập nhật của trình duyệt. Do đó, chúng ta sẽ bỏ lỡ khung hình hoặc thực hiện quá nhiều thao tác trong mỗi khung hình. Điều đó có thể dễ dàng dẫn đến cảm giác giật và không tự nhiên cho trang web, 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 thu thập 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 đôi chút, mặc dù không cải thiện nhiều. 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 quá tốn kém, nhưng trong các trường hợp sử dụng khác thì thực sự có thể tốn kém. Hiện tại, ít nhất chúng ta chỉ thực hiện thao tác một bố cục trong mỗi khung.

Công cụ của Chrome cho nhà phát triển với các sự kiện cuộn được gỡ bỏ.
DevTools cho thấy nội dung hiển thị lớn và nhiều bố cục do sự kiện kích hoạt trong một khung hình.

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 điều 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à cập nhật hình ảnh. Vấn đề là bạn đã chuyển từ việc cố gắng buộc cập nhật bằng 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 đó. Bạn ngọt ngào không?

Vấn đề chính của phương pháp này, dù requestAnimationFrame hay không, là về cơ bản chúng ta có một lớp cho toàn bộ trang và khi di chuyển các phần tử hình ảnh này xung quanh, chúng ta sẽ yêu cầu các lớp vẽ lại lớn (và tốn kém). Thông thường, việc nói bức tranh 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ể thực hiện bất kỳ tác vụ nào khác và chúng tôi thường vượt quá ngân sách khung hình là 16 mili giây và mọi thứ vẫn bị giật.

Phương án 2: Sử dụng các phần tử DOM và biến đổi 3D

Thay vì sử dụng vị trí tuyệt đối, một phương pháp khác có thể áp dụng là áp dụng 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 áp dụng các biến đổi 3D được cấp một lớp mới cho mỗi phần tử và trong các trình duyệt WebKit, nó thường cũng gây ra chuyển đổi qua trình tổng hợp phần cứng. Ngược lại, ở Phương án 1, chúng ta có một lớp lớn cho trang cần vẽ lại khi có thay đổi, và tất cả việc 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 nhau: 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à nhiều biến đổi hơn trên các phần tử, chúng ta sẽ không cần vẽ lại lớp và 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 lần mọi người chỉ sử dụng bản tấn công -webkit-transform: translateZ(0); và thấy sự cải thiện hiệu suất kỳ diệu, nhưng mặc dù cách này vẫn hoạt động như hiện nay thì vẫn có các vấn đề:

  1. Hộp cát về quyền riêng tư không tương thích trên nhiều trình duyệt.
  2. Mô-đun này buộc trình duyệt kiểm soát bằng cách tạo một lớp mới cho mỗi phần tử đã chuyển đổi. Việc có quá nhiều lớp có thể gây ra nút thắt cổ chai khác về hiệu suất, vì vậy, hãy dùng một cách thận trọng!
  3. Nút này đã bị tắt đối với một số cổng WebKit (dấu đầu dòng thứ tư từ dưới cùng!).

Nếu bạn đi theo lộ trình dịch 3D một cách thận trọng, thì đây 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ừ chuyển đổi 2D như chúng ta thực hiện 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 trước đó là những gì chúng ta sẽ thấy.

Cuối cùng, bạn nên cố gắng tránh sơn ở bất cứ đâu có thể và chỉ di chuyển các thành phần hiện có xung quanh trang. Ví dụ: đây là một phương pháp điển hình trong các trang web thị sai để sử dụng các div chiều cao cố định và thay đổi vị trí nền của chúng để mang lại hiệu ứng. Rất tiếc, điều đó có nghĩa là bạn cần phải sơn lại phần tử trên mỗi lần chuyển và việc này có thể ảnh hưởng đến 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ử đó.

Lựa chọn 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 ở vị trí cố định ở cuối trang để vẽ các hình ảnh đã chuyển đổi. Thoạt nhìn có vẻ như không phải là giải pháp hiệu quả nhất, nhưng thực ra có một vài lợi ích cho phương pháp này:

  • Chúng tôi không còn yêu cầu trình kết hợp nhiều như vậy nữa do chỉ có một phần tử là canvas.
  • Chúng tôi đang xử lý hiệu quả một bitmap tăng tốc phần cứng duy nhất.
  • API Canvas2D rất phù hợp với những kiểu chuyển đổi mà chúng tôi đang muốn thực hiện, tức là việc phát triển và bảo trì sẽ dễ quản lý hơn.

Việc sử dụng phần tử canvas sẽ cung cấp cho chúng ta một lớp mới, nhưng đó chỉ là một lớp, trong khi ở Phương án 2, chúng ta thực sự được cấp một lớp mới cho mọi phần tử được áp dụng hiệu ứng biến đổi 3D, vì vậy khối lượng công việc sẽ tăng lên khi kết hợp tất cả các lớp đó lại với nhau. Đây cũng là giải pháp tương thích nhất hiện nay trong bối cảnh các cách triển khai chuyển đổi khác nhau 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 đang xử lý những hình ảnh lớn (hoặc các phần tử khác có thể dễ dàng được viết 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, đó có thể là giải pháp thích 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 đó sẽ đòi hỏi khả năng tiếp cận (bạn vừa tạo điểm ảnh cho văn bản thành bitmap!) và giờ đây, bạn sẽ phải xử lý việc gói dòng và hàng loạt vấn đề khác. Nếu có thể tránh, bạn thực sự nên tránh sử dụng phương pháp biến đổi ở trên.

Khi chúng ta triển khai đến mức tối đa có thể, không có lý do gì để cho rằng công việc thị sai nên được thực hiện bên trong một thành phần canvas. Nếu trình duyệt hỗ trợ, chúng ta có thể sử dụng WebGL. Điều quan trọng ở đây là WebGL có đường dẫn trực tiếp nhất của tất cả các API đến thẻ đồ họa và, do đó, là ứng cử viên khả thi nhất để bạn đạt được tốc độ 60 khung hình/giây, đặc biệt nếu hiệu ứng của trang web phức tạp.

Phản ứng ngay lập tức của bạn có thể là WebGL quá mức cần thiết hoặc WebGL không phổ biến về mặt hỗ trợ, nhưng nếu bạn sử dụng một thứ gì đó 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 tóm tắt theo 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 hiện đại hoá để 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();
}

Nếu không thích thêm các phần tử phụ vào trang, bạn luôn có thể sử dụng canvas làm phần tử nền trong cả trình duyệt Firefox và WebKit. Tất nhiên là không phổ biến, vì vậy bạn nên thận trọng khi xử lý.

Bạn có quyền lựa chọn

Lý do chính khiến các nhà phát triển mặc định sử dụng các thành phần có vị trí tuyệt đối thay vì bất kỳ tuỳ chọn nào khác chỉ đơn giản là sự đa dạng của dịch vụ hỗ trợ. Điều này, ở một mức độ nào đó, là ảo tưởng, vì các trình duyệt cũ hơn đang được nhắm mục tiêu có thể cung cấp 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 yếu tố có vị thế tuyệt đối chưa chắc đã 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. Chìa khoá để thành công ở đây là tránh tô màu ở bất cứ nơi nào bạn có thể, mà chỉ cần thử và di chuyển các yếu tố xung quanh. Hãy ghi nhớ rằng cách mà trình duyệt WebKit tạo các lớp không nhất thiết tương quan với các công cụ trình duyệt khác, vì vậy hãy chắc chắn rằng bạn thử nghiệm trước khi sử dụng giải pháp đó.

Nếu bạn chỉ nhắm đến cấp trình duyệt cao nhất và có thể hiển thị trang web bằng canvas, đó có thể là lựa chọn tốt nhất cho bạn. Chắc chắn là nếu bạn sử dụng Three.js, bạn sẽ có thể trao đổi và thay đổi giữa các trình kết xuất rất dễ dàng tùy thuộc vào sự hỗ trợ mà bạn yêu cầu.

Kết luận

Chúng tôi đã đánh giá một vài phương pháp tiếp cận để xử lý các trang web thị sai, từ các yếu tố có vị trí tuyệt đối đến sử dụng canvas có vị trí cố định. Tất nhiên, việc triển khai mà bạn thực hiện sẽ phụ thuộc vào những gì bạn đang cố gắng đạt được và thiết kế cụ thể mà bạn đang thực hiện, nhưng bạn nên biết rằng bạn có các lựa chọn.

Và như mọi khi, cho dù bạn thử cách nào đi nữa: đừng đoán mà hãy thử nghiệm.