[프론트엔드 성능 최적화] (3)

yongkini ·2023년 1월 26일
0
post-thumbnail

최적화 방법들

: 오늘은 아래의 두 최적화 방법에 대해 정리해보고자 한다.

이미지 사전로딩

: 저번에도 말한 부분이지만 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 애니메이션 최적화

먼저, 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개가 빠지게 되는 것이다. 그러면 당연히 유저입장에서는 매끄럽지 않은 애니메이션을 보게 될 것이다. 이를 방지하려면 어떻게 해야할까?

  • 하드웨어 가속(GPU를 쓴다)을 이용한다 : 본래 브라우저의 작업은 CPU를 사용하여 이뤄진다. 하지만 앞서 말한 것처럼 CPU가 연속적으로 리플로우, 리페인트의 역할을 수행함과 동시에 화면을 그리다보면 60FPS를 맞추지 못하는 일종의 블로킹 현상이 생기게 될 수 있다. 이 때, 이 CPU의 작업을 분업하는 방법을 쓸 수 있다. 이걸 GPU에서 해준다. 앞서 말한 것처럼 top, left, position 등의 속성을 수정하게 되면 리플로우가 일어난다(background 등을 수정하면 리페인트가 일어난다). 이 때 같은 효과를 주더라도 다른 방법으로 구현할 수 있고, 그와 동시에 GPU를 쓰도록 할 수 있는 css 속성들이 있는데 그중 하나가 transform이다.
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를 쓰면 본래 최적화를 스스로 하는 브라우저로 하여금 결국 아무것도 바뀌지 않고 결국 아무 것도 행하지 않게 한다. 따라서 정말 리소스를 많이 사용하여 최적화가 반드시 필요해 보이는 곳에 쓰는게 좋다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글