HTML5 Canvas 성능저하 및 랙 이슈 해결
개요
현재 개발 중인 포트폴리오 사이트에 이전에 제작했던 토이 프로젝트인 Wave를 추가할 때 발생한 문제이다.
아래는 기존에 제작한 Wave 프로젝트의 간략한 설명이다.
Wave
- HTML5 + Javascript
- App : canvas 생성, 메서드 생성, WaveGroup 생성
- WaveGroup : 지정한 wave와 point 개수에 맞춰 파도 각각의 색상과 포인트 지정 후 Wave 생성
- Wave : canvas context에 파도를 그린다. 화면 크기 변화 시 각 파도를 resize 한다.
- WaveGroup : 지정한 wave와 point 개수에 맞춰 파도 각각의 색상과 포인트 지정 후 Wave 생성
기존 프로젝트의 경우 파도의 갯수와 파도의 높이를 결정하는 waveCount와 pointCount를 WaveGroup 내부에서 하드 코딩하여 미리 지정하고 생성하여, 처음 한 번 canvas를 그릴 때 WaveGroup이 생성되면 처음 만든 Wave 인스턴스들을 그대로 사용할 수 있었다.
이번에 포트폴리오에 소개하면서, 만약 프로젝트를 보는 사람이 직접 파도의 갯수와 높이 (waveCount와 pointCount)를 바꾸면서 변화를 볼 수 있다면 더 매력적으로 보일 것 같았다.
2개의 변수를 각각 useEffect에 등록하고, 슬라이더를 통해 값을 변경하여 여러 형태의 파도를 볼 수 있도록 코드를 수정했다.
useEffect(() => {
// useEffect to Clean-up when data change
new App(themeColor, waveCount, pointCount)
}, [themeColor, waveCount, pointCount])
배포 이후 정상적으로 작동하는 것 같았지만, 테스트를 위해 슬라이더를 여러 번 조작해보니 파도가 렉이 걸려 천천히 움직이고, 웹사이트의 전반적인 성능이 저하되었다.
문제는 해당 wave 페이지를 나와 다른 페이지로 가도 JS의 가비지콜렉터가 동작하지 않는 것인지, 새로고침을 하기 전까진 성능 이슈가 계속해서 관찰되었다.
원인
크롬의 개발자 콘솔에서 성능을 테스트해보자, 확실히 레이아웃 변경(point 및 wave 개수 변경) 시마다 약 3000ms 정도의 작업시간이 소요되고, 이후 계속해서 프레임 저하가 발생하는 걸 확인할 수 있었다.
결론적인 원인은, 슬라이더를 통해 useEffect 훅이 작동하고 새로운 wave가 생성되어도, 이전의 canvas의 값들이 메모리에서 지워지지 않았기 때문에 발생하는 문제였다.
해결
useEffect clean-up
처음 생각해낸 해결책은, Wave 컴포넌트가 unmount될 때마다 이전에 생성한 waveCanvas가 지워지도록 하기 위해 useEffect가 canvas를 지우는 함수를 리턴하게 하는 방법이었다.
이때, JS의 가비지 콜렉터는 직접 조작할 수 없기 때문에 클래스를 통해 생성된 인스턴스는 해당 인스턴스를 null로 재지정하고, DOM에 appendChild를 통해 생성한 canvas Element는 다시 removeChild를 통해 지워줘야 한다.
useEffect(() => {
// useEffect to Clean-up when data change
const waveApp = new App(themeColor, waveCount, pointCount)
return () => {
const waveCanvas = document.getElementById('wave-canvas')
if (waveCanvas.hasChildNodes()) {
waveApp.canvas.parentNode.removeChild(waveApp.canvas)
waveApp.canvas = null
}
}
}, [themeColor, waveCount, pointCount])
하지만 해당 방법은 canvas element를 직접 지우는 방식이기 때문에, 여러 문제가 발생했다.
해당 에러 메시지처럼 canvas가 null이 되어 파도를 생성하지 못하는 경우가 발생했다.
이후 여러 방법으로 코드를 수정하려 했지만, 오히려 초기 캔버스가 아예 안 그려지거나 같은 캔버스 위에 다른 파도가 그려지거나 초기의 성능 저하가 남아있는 등 문제가 해결되지 않았다.
문제 해결을 위해 Canvas에 대해 공부를 하던 중, Canvas API 문서와 스택오버플로우에서 관련 내용을 확인할 수 있었다.
Canvas 성능 저하를 막기 위해 고려해야 할 조건
1. 하나의 레이어는 하나의 캔버스에 그린다.
2. 캔버스를 새로 만들거나 지우는 걸 반복하는 대신 캔버스의 내용을 지우고 다시 그리는 것이 효율적이다.
이 프로젝트의 경우 1번은 이미 지키고 있었으나, 새로 파도를 그릴 때마다 Canvas Element를 삭제하고 새로 생성하는 방식으로 코드를 작성하고 있었다.
clearRect + canvas 재활용
기존 캔버스 생성 코드는 아래와 같다.
constructor(themeColor, tideCount, point) {
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
const waveCanvas = document.getElementById('wave-canvas')
waveCanvas.appendChild(this.canvas)
this.waveGroup = new WaveGroup(themeColor, tideCount, point)
...
}
해당 코드를 document의 canvas Element를 체크하고 이미 존재한다면 (최초 렌더링이 아니라 이미 파도가 그려져 있는 상태라면) 해당 캔버스를 공백상태로 만든 후 다시 그 위에 파도를 그리도록 아래와 같이 수정했다.
또한 useEffect 훅의 clean-up 부분도 삭제하여 초기 상태로 롤백했다.
constructor(themeColor, tideCount, point) {
// canvas 생성 혹은 재활용
const waveCanvas = document.getElementById('wave-canvas')
if (!waveCanvas.hasChildNodes()) {
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
waveCanvas.appendChild(this.canvas)
} else {
this.canvas = waveCanvas.firstElementChild
this.ctx = this.canvas.getContext('2d')
this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight)
}
...
}
해당 코드를 적용한 이후, 다른 페이지의 성능 저하 이슈가 모두 사라지고 Wave의 기능 또한 문제 없이 잘 구현되었다.
참고문헌