HTML5 캔버스 렌더링 최적화 기법과 적용 사례

0

HTML5 캔버스는 브라우저에서 2D 도형, 이미지, 텍스트를 직접 그릴 수 있는 강력한 도구입니다. AG Grid의 자바스크립트 차트 라이브러리인 AG Charts는 대규모 데이터(10만 건 이상)를 효율적으로 처리하기 위해 캔버스를 선택하였으며, 다양한 최적화 기법을 적용하여 성능을 극대화했습니다. 이번 분석에서는 AG Charts가 캔버스 렌더링 성능을 최적화하기 위해 사용한 주요 기법들을 자세히 살펴보겠습니다.

pexels

HTML5 캔버스의 이해

HTML5 캔버스는 `<canvas>` 요소를 통해 다양한 그래픽을 그릴 수 있으며, 주로 2D 렌더링 컨텍스트(`getContext(“2d”)`)를 사용합니다. AG Charts는 3D 렌더링을 사용하지 않고 2D 렌더링에 집중하여 성능을 최적화했습니다. 캔버스는 도형, 선, 호, 이미지, 텍스트 등을 그리기 위한 다양한 메서드를 제공하며, 이를 통해 복잡한 그래픽을 효율적으로 렌더링할 수 있습니다.

기본 캔버스 예제

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// 현재 상태 저장
ctx.save();
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 100, 100);
// 가장 최근에 save() 메서드로 저장한 상태로 되돌리기
ctx.restore();
ctx.fillRect(150, 40, 100, 100);

위 예제는 캔버스에 두 개의 녹색 사각형을 그리는 간단한 코드로, `save()`와 `restore()` 메서드를 사용하여 상태를 관리하는 방법을 보여줍니다.

캔버스 렌더링의 주요 과제

캔버스 렌더링 시 `beginPath`, `arc`, `fill`, `stroke` 등의 메서드가 반복적으로 호출되면 성능 저하가 발생할 수 있습니다. 특히 대규모 데이터셋을 처리할 때 메서드 호출이 프레임당 수천 번 또는 수십만 번 일어나면, 메인 스레드의 부하가 커져 사용자가 체감할 수 있는 성능 저하가 발생합니다.

예를 들어, 10만 개의 데이터 포인트를 렌더링할 때 각 메서드를 개별적으로 호출하면 선형 시간 복잡도(O(n))로 인해 렌더링 속도가 느려집니다. 실제로 크롬에서 10만 개의 데이터 포인트를 렌더링하는 데 약 287.1ms가 소요되었습니다.

성능 최적화 기법

1. 배치 렌더링 (Batch Rendering)

배치 렌더링은 여러 그리기 호출을 하나의 그룹으로 묶어 한 번에 실행하는 최적화 기법입니다. 이를 통해 개별 호출의 오버헤드를 줄이고 렌더링 시간을 단축할 수 있습니다.

export const drawBatched = ({ ctx, data, size, fill, stroke }) => {
 ctx.save();

 const r = size / 2;
 ctx.beginPath(); // 한 번 호출
 data.forEach(({ x, y }) => {
   ctx.moveTo(x + r, y);
   ctx.arc(x, y, r, 0, Math.PI * 2);
 });

 // 스타일 상태 변경 사항 적용
 ctx.fillStyle = fill;
 ctx.strokeStyle = stroke;
 ctx.lineWidth = 2;

 // 경로를 한 번에 일괄 처리하여 그리기
 ctx.fill();
 ctx.stroke();

 ctx.restore();
};

배치 렌더링을 적용하면 10만 개의 데이터 포인트 렌더링 시간을 287.1ms에서 15.4ms로 크게 줄일 수 있습니다. 단점으로는 여러 도형이 겹칠 경우 “뭉개짐” 효과가 발생할 수 있습니다.

2. 오프스크린 캔버스 API를 활용한 스프라이트 렌더링

오프스크린 캔버스는 메모리 내에서만 렌더링되는 캔버스를 제공하며, DOM에 종속되지 않고 독립적으로 동작할 수 있습니다. 이를 통해 렌더링 작업을 워커에서 처리하여 메인 스레드의 부하를 줄일 수 있습니다.

export const drawOffscreen = ({ canvasCtx, offscreenCanvasCtx, data }) => {
  canvasCtx.save();
 
  offscreenCanvasCtx.fillStyle = fill;
  offscreenCanvasCtx.strokeStyle = stroke;
  offscreenCanvasCtx.lineWidth = strokeWidth;
 
  const center = (size + strokeWidth) / 2;
  const r = size / 2;
  const sSize = size + strokeWidth;
 
  // 한 번 호출
  offscreenCanvasCtx.beginPath();
  offscreenCanvasCtx.arc(center, center, r, 0, Math.PI * 2);
  offscreenCanvasCtx.fill();
  offscreenCanvasCtx.stroke();
 
  // 각 데이터 포인트에 대해 오프스크린 캔버스의 내용을 메인 캔버스에 그리기
  data.forEach(({ x, y }) => {
    canvasCtx.drawImage(
      offscreenCanvasCtx, 
      x - center, 
      y - center, 
      sSize, 
      sSize
    );
  });
 
  canvasCtx.restore();
};

이 방법을 적용하면 10만 개의 데이터 포인트 렌더링 시간을 65.7ms로 줄일 수 있습니다. 배치 렌더링보다 약간의 성능 비용이 발생하지만, 결과물의 품질에는 영향을 미치지 않습니다.

3. 변경 감지 (Change Detection)

AG Charts는 트리 기반 장면(scene) 그래프를 사용하여 변경된 노드만 다시 그리는 방식으로 성능을 최적화했습니다. `dirty` 플래그를 통해 변경된 속성이 있는 노드와 관련된 부모 그룹을 식별하고, 해당 부분만 재렌더링함으로써 불필요한 그리기 작업을 최소화했습니다.

이 접근 방식은 전체 렌더링 성능을 크게 향상시키며, 필요한 부분만 효율적으로 다시 그릴 수 있게 합니다.

성능 비교 및 요약

아래 표는 10만 개의 데이터 포인트를 캔버스에 그릴 때 각 최적화 방식의 성능 차이를 보여줍니다:

렌더링 방식 렌더링 시간 (ms)
개별 렌더링
287.1
배치 렌더링
15.4
오프스크린 렌더링
65.7

배치 렌더링이 가장 빠르지만, 스프라이트 렌더링과 변경 감지를 결합하면 성능과 품질의 균형을 맞출 수 있습니다.

결론

HTML5 캔버스의 성능 최적화는 AG Charts와 같은 고성능 차트 라이브러리에서 필수적입니다. 배치 렌더링, 오프스크린 캔버스, 변경 감지 등의 기법을 통해 대규모 데이터를 효율적으로 처리할 수 있으며, 상황에 따라 적절한 최적화 방식을 선택하는 것이 중요합니다. 이러한 최적화 과정을 통해 AG Charts는 높은 성능과 품질을 유지하며 사용자에게 우수한 시각화 경험을 제공합니다.

참고 자료: AG Grid, “Optimising HTML5 Canvas Rendering: Best Practices and Techniques”

답글 남기기