시차'

소개

최근 패럴랙스 사이트가 대세입니다. 다음 내용을 살펴보세요.

생소하다면 스크롤에 따라 페이지의 시각적 구조가 변경되는 사이트입니다. 일반적으로 페이지 스케일 내의 요소는 페이지의 스크롤 위치에 비례하여 회전하거나 이동합니다.

데모 시차 페이지
시차 효과가 적용된 데모 페이지

시차 사이트를 좋아하는지 여부는 중요하지 않습니다. 하지만 시차 사이트가 성능의 블랙홀이라고 자신 있게 말할 수 있습니다. 그 이유는 스크롤 방향에 따라 스크롤할 때 새 콘텐츠가 화면의 상단이나 하단에 표시되는 경우에 브라우저가 최적화되는 경향이 있고, 일반적으로 스크롤 중에 시각적으로 변화가 거의 없을 때 브라우저가 가장 잘 작동하기 때문입니다. 패럴랙스 사이트의 경우 페이지 전체에 큰 시각적 요소가 여러 번 변경되므로 브라우저에서 전체 페이지를 다시 페인트해야 하므로 드물게 발생합니다.

다음과 같이 시차 사이트를 일반화하는 것이 합리적입니다.

  • 위아래로 스크롤할 때 위치, 회전 및 배율을 변경하는 배경 요소
  • 일반적인 위에서 아래로 스크롤되는 텍스트나 작은 이미지와 같은 페이지 콘텐츠입니다.

앞서 스크롤 성능과 앱의 반응성을 개선할 수 있는 방법을 살펴보았으며, 이 도움말은 이를 토대로 구축되어 아직 사용해 보지 않았다면 읽어볼 가치가 있습니다.

그렇다면 문제는 패럴랙스 스크롤 사이트를 빌드하면서 비용이 많이 드는 리페인트에 종속되어 있는지, 아니면 성능을 극대화하기 위해 취할 수 있는 대안이 있는지입니다. 옵션을 살펴보겠습니다.

옵션 1: DOM 요소 및 절대 위치 사용

이는 대부분의 사람들이 택하는 기본적인 접근 방식인 것 같습니다. 페이지 내에 여러 요소가 있으며 스크롤 이벤트가 발생할 때마다 이를 변환하기 위해 여러 시각적 업데이트가 실행됩니다.

프레임 모드에서 DevTools 타임라인을 시작하고 여기저기 스크롤하면 비용이 많이 드는 전체 화면 페인트 작업이 있음을 알 수 있으며, 많이 스크롤하면 단일 프레임 내에서 여러 스크롤 이벤트를 볼 수 있습니다. 각 스크롤 이벤트는 레이아웃 작업을 트리거합니다.

디바운스 스크롤 이벤트가 없는 Chrome DevTools
하나의 프레임에 큰 페인트와 여러 이벤트에 의해 트리거되는 레이아웃을 보여주는 DevTools

기억해야 할 중요한 점은 60fps (일반적인 모니터 새로고침 빈도인 60Hz와 일치)에 도달하려면 16ms가 넘으면 모든 작업을 완료할 수 있다는 것입니다. 이 첫 번째 버전에서는 스크롤 이벤트가 발생할 때마다 시각적 업데이트를 수행하지만, 이전 도움말에서 requestAnimationFrame을 사용한 더 가볍고 평균적인 애니메이션스크롤 성능에 관해 다루었듯이 이는 브라우저의 업데이트 일정과 일치하지 않으므로 프레임이 누락되거나 각 프레임 내에서 너무 많은 작업을 실행합니다. 이로 인해 사이트에 품질이 나쁘고 부자연스러운 느낌이 들기 쉬우며, 이는 사용자를 실망시키고 새끼 고양이에게 불만족스러워하는 결과로 이어질 수 있습니다.

업데이트 코드를 스크롤 이벤트에서 requestAnimationFrame 콜백으로 이동하고 스크롤 이벤트의 콜백에서 스크롤 값을 간단히 캡처해 보겠습니다.

스크롤 테스트를 반복하면 그다지 크지 않더라도 약간의 개선 효과를 경험할 수 있습니다. 스크롤로 트리거하는 레이아웃 작업이 그렇게 비용이 많이 들지는 않지만 다른 사용 사례에서는 실제로 그렇게 할 수 있기 때문입니다. 이제 적어도 각 프레임에서 한 개의 레이아웃 작업만 실행합니다.

디바운스 스크롤 이벤트가 있는 Chrome DevTools
하나의 프레임에 큰 페인트와 여러 이벤트에 의해 트리거되는 레이아웃을 보여주는 DevTools

이제 프레임당 1개 또는 100개의 스크롤 이벤트를 처리할 수 있지만 중요한 점은 requestAnimationFrame 콜백이 실행되고 시각적 업데이트를 실행할 때마다 사용할 최신 값만 저장합니다. 요점은 스크롤 이벤트를 받을 때마다 강제로 시각적 업데이트를 시도하던 방식에서 브라우저에 이를 위한 적절한 창을 제공하도록 요청하는 방식으로 바뀌었다는 것입니다. 너는 다정하지 않아?

requestAnimationFrame 여부와 상관없이 이 접근 방식의 주요 문제는 기본적으로 전체 페이지에 하나의 레이어가 있으며 이러한 시각적 요소를 이리저리 옮기면 비용이 많이 드는 대규모 다시 페인트가 필요하다는 점입니다. 일반적으로 페인팅은 변경되지만 차단 작업입니다. 이는 브라우저가 다른 작업을 할 수 없으며 16ms라는 프레임 예산을 초과하여 종종 버벅거림이 발생한다는 것을 의미합니다.

옵션 2: DOM 요소 및 3D 변환 사용

절대 위치를 사용하는 대신, 요소에 3D 변환을 적용하는 또 다른 접근 방식을 취할 수 있습니다. 이 상황에서 3D 변환이 적용된 요소에는 요소별로 새 레이어가 주어지며, WebKit 브라우저에서는 종종 하드웨어 컴포지터로 전환되기도 합니다. 반면에 옵션 1에서는 페이지에 변경사항이 있을 때 다시 페인트해야 하는 큰 레이어가 하나 있고 모든 페인팅 및 합성은 CPU에서 처리되었습니다.

즉, 옵션을 사용하면 결과가 다릅니다. 3D 변환을 적용하는 모든 요소에는 하나의 레이어가 있을 수 있습니다. 이 지점에서 할 수 있는 작업이 요소에 추가 변환이라면 레이어를 다시 그릴 필요가 없으며 GPU는 요소를 이동하고 최종 페이지를 함께 합성하는 작업을 처리할 수 있습니다.

많은 경우 사람들이 -webkit-transform: translateZ(0); 해킹을 사용하면 마법 같은 성능 개선을 경험하게 되지만 현재 이 방식은 다음과 같은 문제가 있습니다.

  1. 브라우저 간 호환되지 않습니다.
  2. 변환된 모든 요소에 대해 새 레이어를 생성하여 브라우저의 손을 잡습니다. 레이어가 많으면 다른 성능 병목 현상이 발생할 수 있으므로 가급적 사용하지 마세요.
  3. 일부 WebKit 포트에서 사용 중지되었습니다 (하단에서 네 번째 글머리 기호).

3D 변환 경로를 사용할 때는 주의해야 합니다. 일시적인 문제 해결 방법이기 때문입니다. 이상적으로 말하면 3D에서와 마찬가지로 2D 변환에서 유사한 렌더링 특성을 볼 수 있습니다. 브라우저는 경이로운 속도로 발전하고 있습니다. 그 전에 먼저 경험해 보시길 바랍니다.

마지막으로, 가능한 경우 페인트를 피하고 페이지에서 기존 요소를 이동하기만 하면 됩니다. 예를 들어 패럴랙스 사이트에서는 고정 높이 div를 사용하고 배경 위치를 변경하여 효과를 제공하는 것이 일반적입니다. 즉, 모든 패스에서 요소를 다시 페인트해야 하므로 성능 면에서 비용이 발생할 수 있습니다. 대신 가능한 경우 요소를 만들고 (필요한 경우 overflow: hidden로 div 안에 래핑) 간단히 번역해야 합니다.

옵션 3: 고정 위치 캔버스 또는 WebGL 사용

마지막으로 고려할 방법은 페이지 뒷면에 변환된 이미지를 그릴 고정 위치 캔버스를 사용하는 것입니다. 언뜻 보기에는 가장 성능이 뛰어난 솔루션처럼 보이지 않을 수 있지만, 실제로 이 접근 방식에는 다음과 같은 몇 가지 이점이 있습니다.

  • 캔버스에만 요소가 있기 때문에 더 이상 컴포지터 작업이 많이 필요하지 않습니다.
  • 단일 하드웨어 가속 비트맵을 효과적으로 처리하고 있습니다.
  • Canvas2D API는 수행하려는 일종의 변환에 매우 적합하므로 개발 및 유지 관리가 더 용이합니다.

캔버스 요소를 사용하면 새 레이어를 얻을 수 있지만 레이어는 하나에 불과하지만, 옵션 2에서는 실제로 3D 변환이 적용된 모든 요소에 대해 새 레이어가 주어졌기 때문에 이러한 모든 레이어를 합성하는 워크로드가 증가했습니다. 또한 이 방법은 브라우저 간 변환이 서로 다르다는 점을 고려했을 때 현재 가장 호환성이 높은 솔루션입니다.


/**
 * 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));
}

이 접근 방식은 큰 이미지 (또는 캔버스에 쉽게 쓸 수 있는 기타 요소)를 다루는 경우 실제로 효과적이며 큰 텍스트 블록을 다루는 것은 더 까다로울 수 있지만 사이트에 따라 가장 적합한 솔루션일 수 있습니다. 캔버스의 텍스트를 처리해야 하는 경우 fillText API 메서드를 사용해야 하지만, 접근성을 희생해야 합니다 (텍스트를 비트맵으로 래스터화함). 따라서 이제 줄바꿈 및 기타 온갖 문제를 처리해야 합니다. 이를 피할 수 있다면 실제로 그렇게 해야 하며, 위의 변환 접근 방식을 사용하여 더 나은 결과를 얻을 수 있습니다.

이것을 최대한 고려하고 있으므로 시차 작업이 캔버스 요소 내에서 실행되어야 한다고 생각할 이유가 없습니다. 브라우저에서 지원하는 경우 WebGL을 사용할 수 있습니다. 여기서 핵심은 WebGL이 그래픽 카드로 모든 API에 대해 가장 직접적인 경로를 가지고 있으므로 특히 사이트의 효과가 복잡한 경우 60fps를 달성할 가능성이 가장 높다는 것입니다.

즉각적으로 WebGL이 과도하거나 지원 측면에서 어디에나 없을 수도 있지만 Three.js와 같은 것을 사용하면 언제든지 캔버스 요소를 사용할 수 있으며 코드가 일관되고 친숙한 방식으로 추상화됩니다. Modernizr를 사용하여 적절한 API 지원 여부를 확인하기만 하면 됩니다.

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

페이지에 요소를 추가하는 것을 원하지 않는 경우 Firefox 및 WebKit 기반 브라우저 모두에서 언제든지 캔버스를 배경 요소로 사용할 수 있습니다. 이는 당연하게 볼 수 있는 일이 아니므로 평상시처럼 주의해서 처리해야 합니다.

원하는 대로 선택할 수 있습니다.

개발자가 다른 옵션이 아닌 절대 위치로 배치된 요소를 기본 설정하는 주된 이유는 단순히 지원이 보편적이기 때문일 수 있습니다. 이는 표적이 되는 이전 브라우저가 매우 열악한 렌더링 환경을 제공할 수 있기 때문에 어느 정도는 거짓입니다. 오늘날의 최신 브라우저에서도 절대 위치로 배치된 요소를 사용한다고 해서 반드시 우수한 성능이 보장되지는 않습니다.

확실히 3D 종류인 변환은 DOM 요소와 직접 작업하고 견고한 프레임 속도를 달성할 수 있는 기능을 제공합니다. 여기서 성공의 핵심은 가능한 한 페인트를 피하고 단순히 요소를 이리저리 옮기는 것입니다. WebKit 브라우저가 레이어를 생성하는 방식이 반드시 다른 브라우저 엔진과 상관 관계가 있는 것은 아니므로, 해당 솔루션을 적용하기 전에 반드시 테스트해야 합니다.

최상위 브라우저만을 목표로 하며 캔버스를 사용하여 사이트를 렌더링할 수 있는 경우 이 옵션이 가장 적합할 수 있습니다. 물론 Three.js를 사용하는 경우 필요한 지원에 따라 렌더기 간에 매우 쉽게 전환하고 변경할 수 있어야 합니다.

결론

Google에서는 절대 위치로 배치된 요소에서 고정 위치 캔버스를 사용하는 것까지 시차 사이트를 처리하는 몇 가지 접근 방식을 평가했습니다. 물론 구현하는 방법은 달성하고자 하는 부분과 작업 중인 디자인에 따라 달라지지만, 여러 옵션이 있다는 것을 알아두는 것이 좋습니다.

어떤 접근 방식을 사용하든 항상 그렇듯이 추측하지 말고 테스트하세요.