: 오늘은 아래의 두 최적화 방법에 대해 정리해보고자 한다.
: 저번에도 말한 부분이지만 LightHouse의 Cumulative Layout Shift(CLS)
지표와 관련이 있는 최적화 기법이다(명확히 관계 있는 것은 Opportunity에 나오는 Preload Largest Contentful Paint Image
이다. 위 사진 참고). 예를 들어, 특정 홈페이지를 구현하면서 굉장히 큰 이미지 파일을 가져와서 banner
형태로 쓰는 기획이 있다고 해보자. 이 때, 이 페이지가 랜딩 페이지면 어쩔 수 없지만, 랜딩 페이지가 아닌 랜딩 페이지에서 이동하는 페이지에 있다고 해보자. 아무런 최적화를 하지 않았다면 이 페이지(배너가 있는 페이지)로 갔을 때 배너의 img src="배너 자료의 주소"에 따라 이미지 파일을 다운로드 하고 렌더링할 것이다. 이 때, 앞서 말한 것처럼 이 배너 이미지 파일이 상당히 크기 때문에 banner 부분에 min-height 등을 걸지 않았다면 앞서 말한 CLS 측면에서 유저 경험이 안좋을 것이다. 추가로 애초에 이 이미지를 로딩하는데 시간이 걸리므로 유저 입장에서는 min-height를 걸었어도 흰색의 빈공간을 먼저 보게될 것이다.
: 이를 최적화하기 위해서는 이 페이지에 들어가기 전에 이 이미지를 로딩해놓고 대기하는 방법을 쓸 수 있다. 그렇다면 언제? 이건 앞서 말한 컴포넌트 지연 및 사전 로딩 부분에서도 했었는데, 그 타이밍은 해당 기획에 따라 천차만별일 것이다. 예를 들어, 이 페이지를 가기 위해서 특정 a tag 버튼
을 클릭해야 한다면 이 버튼에 mouseEnter 이벤트로 이 이미지를 사전 로딩할 수 있을 것이고, 혹은 그 버튼을 onClick 했을 때 로딩해도 될 것이며, componentDidMount 시점(App.js의)에 해도 될 것이다. 이처럼 타이밍은 기획에 따라 적용하면 된다. 그러면 실제로 이미지를 사전 로딩하는 방법을 알아보자.
여기서는 위에서 말한 componentDidMount 시점에 한다고 가정하고 해보자.
useEffect(() => {
const img = new Image();
img.src = "사전 로딩하고자 하는 이미지의 src" (as typeof string);
}, []);
위와 같이 해주면 특정 이미지를 불러올 때 즉, 배너 이미지를 불러올 때 이미 이 이미지를 사전에 로딩했으므로
다시 다운받지 않고 그 이미지를 그대로 쓴다(캐싱 해놓고).
이 때, 앞서 말한 것처럼 onClick or onMouseEnter 등을 썼을 때 그러면 click or hover 될 때마다 다운을 받나?라고
생각할 수 있지만 그렇지 않다. 한번 다운을 받은 것은 캐싱하고 있기 때문에 다시 받지 않는다.
이 원리를 이용하면 이미지를 사전 로딩해서 쓸 수 있다
먼저, css로 애니메이션 효과를 만들다보면, 쟁크(jank)
현상이 일어날 때가 있다(끊김 현상을 말한다). 이 얘기를 하기전에 FPS(Frame Per Seconds)
에 대해서 말해보면, 대략 유저들은 60FPS 정도가 될 때 사용자 경험 측면에서 편안함을 느낀다고 한다. 이는 1초에 60번 화면을 그린다는 말로 이해하면 된다. 비유해보면 영화를 보여주는 원리를 생각해보자. static한 이미지를 굉장히 빠른 속도로 바꾸다보면 그 static한 이미지들이 연결돼서 실제 움직이는 영상처럼 보이는 원리로 영화를 보여준다. 이처럼 브라우저도 똑같은 원리로 화면을 보여준다. 이 때 브라우저 렌더링 원리를 생각해보면, 유저가 특정 인터랙션을 일으켰을 때 리플로우, 리페인트 과정을 거쳐 리렌더링을 하게 된다. 이 때 연속적으로 일어나는 리렌더링이 있다고 했을 때 requestAnimationFrame
을 써서 계속해서 좌표값을 갱신하며
** fps 체킹하면서 개발하는 방법
FPS미터를 실행하려면 크롬 개발자 도구에서 Ctrl+Shift+P를 눌러서 명령 메뉴를 실행시킨 뒤 Show frame Per seconds meter를 실행해주면 된다. 실행 후 크롬 FPS미터가 브라우저 위에 성생 된 걸 볼 수 있다.
position: absolute;
top: 0;
left:0;
css 속성을 가진 특정 dom을 움직인다고 가정해보자. 이렇게 되면 top, left 속성은 리플로우를 일으키는 속성이기 때문에(cf: visibility : hidden 등은 리플로우를 일으키지 않는다) 엄청난 속도로 리플로우를 계속해서 브라우저가 수행하게 된다. 리플로우든, 리페인트든 적은 비용이 아니기 때문에 이게 연속으로 일어난다고 하면 아까 말한 60FPS를 맞출 수 없게된다. 왜냐하면 초당 60번의 static 한 이미지를 그리면서 연속된 이미지를 만드는게 브라우저의 역할인데 그 역할 사이사이에 리플로우, 리페인트의 역할이 연속적으로 추가된다면 예를 들어, 39번째 이미지를 그릴 때 리플로우 작업 때문에 그 역할을 수행하지 못할 수 있다. 즉, 연속적인 static 한 이미지 중에 n개가 빠지게 되는 것이다. 그러면 당연히 유저입장에서는 매끄럽지 않은 애니메이션을 보게 될 것이다. 이를 방지하려면 어떻게 해야할까?
width: ${({ width }) => width}%;
위의 코드는 styled-components를 사용한 케이스인데 포인트는 width 변수가 바뀔 때마다 width 속성이 실제로 바뀐다는 것이다. 이 때, 앞서 말했듯이 width는 리플로우를 일으키는 속성이기 때문에
(** 이 때 리플로우 = DOM + CSSOM => 렌더트리 => 레이아웃팅 => 페인트 => 컴포짓 이고, 리페인트는 리플로우에서 레이아웃팅을 뺀 것이다)
이렇게 작업을 하면 width 변할 때마다 브라우저의 작업에 방해를 주게된다. 이 떄, 이에 대한 구현을
width: 100%;
transform: scaleX(${({ width }) => width / 100});
transform-origin: center left;
transition: transform 1.5s ease;
위와 같이 바꿔서 하게 되면 해당 부분을 별개의 레이어로 분리하게 되고, 이에 대한 처리는 GPU가 맡아서 하게 된다. 그에 따라 CPU의 부하를 분담할 수 있다. 추가적인 예시로 앞서 말한
position: absolute;
top: 0;
left:0;
이 상태의 dom 을 매프레임에 좌표값을 갱신하며 위치를 이동 시킬 때 top, left 속성을 이용하지 않고(리플로우를 일으키기 때문) transform : translate()
를 이용하게 되면 GPU를 써서 효율화할 수 있다. 이 때, 이 transform을 쓰는 방법에 있어서 한번더 효율화를 할 수 있는데
- translate() 는 처음부터 레이어를 분리하지 않고 변화가 일어나는 순간 레이어를 분리한다.
- 반면에 transform : translate3d() 또는 scale3d()와 같은 3d 속성들 or will-change 속성은 처음부터 레이어를 분리해두기 때문에 변화에 더욱 빠르게 대처할 수 있다.
- 물론 레이어가 너무 많아지면 그만큼 메모리를 많이 쓰기 때문에 오히려 비효율적이 될 수 있음을 주의.
위와 같다. 하지만 주의할 점은 translate3d()는 일종의 핵(?)이다. 거기에 쓸 것이 아니지만 gpu를 써줘! 하고 핵처럼 쓰는 것이기 때문이다. 그러면 더 좋은 방법은 없을까?
** will-change
: will-change 속성을 사용해 앞으로 일어날 변경에 관해 브라우저에게 알려주고자 할때는 대상이 되는 엘리먼트에 아래와 같이 CSS 코드를 작성하면 된다.
will-change: transform;
변형 처리 외에도 스크롤 위치(표시되고 있는 윈도우 내의 엘리먼트 위치, 화면 내에 엘리먼트가 얼마나 보이고 있는지), 컨텐츠 등 기타 복수의 CSS 속성을 변경할 때에는 그 대상이 되는 속성 이름을 지정하여 브라우저에 변경 의사를 전달할 수 있다. 1개의 엘리먼트에 여러개의 값을 변경할 생각이라면 콤마(,)로 구분하여 기술할 수 있다. 예를들어 한 엘리먼트를 애니메이션 시키는 동시에 위치를 변경하고자하는 경우는 다음과 같이 선언하면 된다.
will-change: transform, opacity;
무엇을 변경하고 싶은지를 정확하게 기술하면 그 변경에 대비한 최적화를 브라우저가 시행한다. 핵을 사용해 불필요한 비용을 발생시키는 레이어를 브라우저에게 억지로 생성시키는 방법보다 이 방법이 명확히 고속화에 도움이된다.
하지만 모든건 과유불급(?)이기에
*,
*::before,
*::after {
will-change: all;
}
이렇게 모든 부분에 will-change를 쓰면 본래 최적화를 스스로 하는 브라우저로 하여금 결국 아무것도 바뀌지 않고 결국 아무 것도 행하지 않게
한다. 따라서 정말 리소스를 많이 사용하여 최적화가 반드시 필요해 보이는 곳에 쓰는게 좋다.