본문 바로가기
우아한 테크코스

프론트엔드 성능개선2

by 해-온 2023. 9. 6.

 

3️⃣ 같은 건 매번 새로 요청하지 않기

  • CDN을 적용하고, 한 번 요청한 리소스는 CDN 캐시에서 불러와야 한다.
  • GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.

도구

키워드

  • CDN
  • HTTP Cache
  • Cache Policy
  • memoization

 


캐시란?

캐시란 어떠한 데이터를 임시 저장하는 장소를 말한다.

만약 서버로부터 리소스를 받기 위해 API 통신을 한다고 가정해 보자.

이미 존재하는 데이터를 요청하는 것은 불필요한 요청이다.

이 경우 서버의 부하를 높이고 웹 성능이 저하되는 문제가 발생한다.

 

캐싱은 리소스의 복사본을 저장하고 있다가 요청 시 해당 복사본을 반환한다.

따로 서버로부터 리소스를 다시 다운 받지 않아도 돼 성능이 향상된다.

 

 


 

🐶 CDN을 적용하고, 한 번 요청한 리소스는 CDN 캐시에서 불러와야 한다.

1. 캐시 정책 설정

AWS CloudFront에서 커스텀으로 만들 수 있는 Cache Policy는 최대 20개이다.

따라서 우테코에서 제공하는 aws에서는 개인별로 Cache Policy를 만들 수 없다.

사전 명세에 따라 s3에서 권장하는 캐시 정책을 설정해 주었다.

 

 

2. 응답 헤더 설정

 

팀별로 하나씩 설정할 수 있어 팀 응답 헤더에 연결하였다.

캐시의 최댓값인 1년을 설정해 주었다.

캐시 기간이 길수록 캐시 적중률 또한 높아진다고 한다.

네트워크 탭을 보면 응답 헤더가 잘 들어간 것을 볼 수 있었다.

 

 

3. 메타 데이터 설정

자주 변경될 JavaScript 파일의 캐시 값은 하루

자주 변경되지 않을 것 같은 static 파일의 경우 1년으로 변경했다.

 

bundle.js가 잘 바뀌었다는 것을 알 수 있다.

 

 

 


 

🐶 GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.

cache storage를 통해 자체적인 캐싱을 구현해 보자.

 

Cache storage

캐시 스토리지는 Wep API로 웹 애플리케이션의 정적 파일 및 데이터를 저장하는 데 사용한다.

네트워크 요청을 통해 가져온 리소스를 로컬에 저장하고 다음 요청 시에 재사용할 수 있도록 한다.

 

getTrending: async function (): Promise<GifImageModel[]> {
    try {
		// 캐시 스토리지를 열고 검색 결과 확인
      const cacheStorage = await caches.open('trending');
      const cachedResponse = await cacheStorage.match(TRENDING_GIF_API);

		// 캐시된 응답이 있는 경우, 해당 응답을 반환
      if (cachedResponse) {
        const gifs: GifsResult = await cachedResponse.json();
        return convertResponseToModel(gifs.data);
      }
	
		// 캐시된 응답이 없는 경우, 네트워크 요청 후 응답을 캐싱하고 반환
      const response = await fetch(TRENDING_GIF_API);

      if (response.ok) {
		// 네트워크 요청 성공 시, 응답을 캐싱
        await cacheStorage.put(TRENDING_GIF_API, response.clone());

        const gifs: GifsResult = await response.json();
        return convertResponseToModel(gifs.data);
      } else {
        throw new Error('네트워크 요청 실패!');
      }
    } catch (e) {
      return [];
    }
  },

 

getTrending 함수가 실행되면, ‘trending’이라는 이름의 캐시 스토리지 객체를 가져온다.

 

가져온 캐시 스토리지 객체에서 cacheStorage.match(TRENDING_GIF_API) 메서드를 사용하여 해당 API 경로에 대한 캐시 된 응답을 확인한다.

 

만약 캐시된 응답이 있는 경우에는 가져오고, 없는 경우 네트워크 요청을 보낸다.

네트워크 요청이 성공하면, 받은 응답과 동일한 복제본을 캐시 스토리지에 저장한다.

 

이렇게 하면 처음 호출될 때에만 실제 API 요청이 발생하고, 이후에는 동일한 API 요청의 경우 저장된 캐시 데이터가 사용된다.

 

 

 


🐶 3단계 완료 후 점수

 


4️⃣ 최소한의 변경만 일으키기

  • 검색 결과 > 추가 로드 시 추가된 목록만 새로 렌더 되어야 한다.
  • Layout Shift 없이 애니메이션이 일어나야 한다.
  • Frame Drop이 일어나지 않아야 한다.
    • (Chrome DevTools 기준) Partially Presented Frame 역시 최소로 발생해야 한다.

도구

  • Chrome DevTools > Performance
  • React Profiler
  • CSS triggers

키워드

  • Browser Rendering Pipeline

 


🐶 검색 결과 > 추가 로드 시 추가된 목록만 새로 렌더 되어야 한다.

React memo를 사용해 Gifitem 컴포넌트를 감싸주었다.

 

export default memo(GifItem);

 

React memo란?

React memo는 컴포넌트를 메모이제이션하는데 사용하는 고차 컴포넌트이다.

일반적으로 React 컴포넌트는 부모 컴포넌트가 업데이트될 때마다 자동으로 리렌더링 된다.

그러나 부모 컴포넌트의 상태나 속성이 변경되지 않았는데 리렌더링 하는 것은 불필요한 작업이다.

React memo를 사용하여 컴포넌트를 감싸면, 해당 컴포넌트는 이전에 전달된 속성과 현재 전달된 속성을 비교하여 변경 여부를 확인한다.

이때 속성이 변경되지 않았다면, 이전에 계산된 결과를 재사용하고 실제로 DOM을 업데이트하지 않는다.

 


🐶 Layout Shift 없이 애니메이션이 일어나야 한다.

브라우저 렌더링 과정

브라우저는 HTML, CSS, JS 등 렌더링에 필요한 리소스를 요청하고 서버로부터 응답을 받는다.

브라우저의 렌더링 엔진은 받은 HTML을 파생하여 DOM 트리를 만들고, CSS를 파싱 하여 CSSOM을 만든다.

그리고 두 트리를 결합하여 Render 트리를 만든다.

 

브라우저의 JavaScript 엔진은 서버로부터 응답한 JavaScript를 파싱 하여 AST(추상 구문 트리)를 생성하고 바이트 코드로 변환하여 실행한다.

이때, JavaScript는 DOM API를 통해 DOM이나 CSSOM을 변경할 수 있다.

이때, 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합된다.

Render 트리를 기반으로 HTML 요소의 레이아웃을 계산하고, 브라우저 화면에 HTML 요소를 페인팅한다.

 

특히 Layout 과정에서는 Render 트리를 재귀적으로 돌며 새롭게 생기거나 바뀐 부분이 없는지 확인하며 각 노드의 위치를 잡는다.

Layout 단계에서는 Reflow라는 작업이 수행되는데 페이지 내부의 모든 엘리먼트에 대해 다시 계산 작업을 수행해야 하므로 많은 비용이 소요된다.

따라서 최대한 Layout 과정이 자주 발생하지 않도록 노력해야 한다.

 

Reflow가 일어나는 대표적인 속성은 아래와 같다

position, width, height, margin, padding, border, border-width, font-size, font-weight, line-height, text-align, overflow

 

Layout Shift란?

Layout Shift란 페이지 콘텐츠가 예기치 않게 이동하는 현상이다.

애니메이션 효과나 요소의 전환을 구현할 때 혹은 콘텐츠를 로딩하는 과정, 반응형 디자인 등에서는 어느 정도의 Layout Shift가 필요하다.

그러나 의도하지 않고 예상치 못한 Layout Shift는 사용자 경험에 부정적인 영향을 줄 수 있다.

따라서 예측 가능하고 일관된 레이아웃을 유지하기 위해 노력을 기울어야 한다.

 

 

따라서 layout 재계산을 하는 속성들을 수정해 보자.

 

 

  • Custom Cursor

performance를 측정했을 때 layout Shift가 발생한다는 경고가 뜬다.

 

 

원래 코드에서는 position을 사용해 계속해서 layout이 일어난다.

 

 

position을 translate으로 수정하였고 더 이상 layout이 발생하지 않는다.

 

performance에서도 더 이상 경고가 나오지 않음을 볼 수 있다.

 

 

translate보다 translate3d가 성능이 더 좋다고 한다.

하드웨어 가속을 통해 GPU를 활용하여 변환 작업을 처리하기 때문이다.

 

 

실제로 translate을 translate로 변경하니 조금 더 빨라진 것을 볼 수 있다.

 

 

  • hover

hover의 경우도 위와 같다.

top을 사용하는 부분을 translate로 바꾸어주었다.

 

 

layout shift도 사라졌다.

 

 

  • help panel

right 속성을 translateX로 변경하였다.

 

 

 


🐶 Frame Drop이 일어나지 않아야 한다.

Frame drop은 애니메이션이나 동영상 재생 등에서 발생하는 현상이다.

화면에 표시되는 연속적인 프레임이 일시적으로 누락되는 것인데 이로 인해 버벅거림이 발생할 수 있다.

현재는 최신 브라우저와 하드웨어의 발전으로 Frame drop이 많이 줄어들었다.

과제 페이지에서도 따로 Frame drop이 발생하지 않아 따로 설정해주진 않았다.

 

 


+추가

빌드 속도를 올리기 위해 Esbuild-Loader를 써보기로 하였다.

 

현재 webpack의 번들링 동작 방식에는 오버헤드가 존재한다.

코드의 일부분만 수정해도 webpack은 처음부터 다시 리빌딩하기 시작한다.

이 과정에서 JavaScript 코드도 bable-loader의 전처리 과정을 다시 수행한다.

따라서 우리는 이 전처리 과정이 수행 시간을 줄여보도록 하자.

bable-loader 대신 esbuild loader를 사용해 보자.

 

사용 방법은 간단하다.

 

npm install esbuild-loader --save-dev

 

esbuild-loader를 설치하고, 기존 webpack의 ts-loader를 esbuild-loader로 대체한다.

 

{
  test: /\.(js|jsx|ts|tsx)$/i,
  exclude: /node_modules/,
//원래 ts-loader가 있던 자리
  loader: 'esbuild-loader'
},

 

그리고 실행을 해보니 빌드 속도가 엄청 줄어들었다.

 

그렇다면 왜 esbuild-loader는 속도가 확연하게 빠를까?

esbuild는 Go 언어로 작성된 모듈 번들러이기 때문이다.

JavaScript의 경우 인터프리터 언어여서 한 줄씩 기계어로 변환을 하지만,

Go의 경우 실행 전 컴파일 단계에서 미리 기계어로 변환하기 때문이다.

 

또한, JavaScript는 싱글 스레드 기반이라 한 번에 한 파일씩 순차적으로 번들링 혹은 트랜스 파일링 되지만, Go의 경우 공유 메모리 환경 아래에서 멀티 스레드 기반으로 동작할 수 있다.

즉, 여러 파일을 동시에 번들링 하거나 트랜스파일링 할 수 있는 것이다.

이러한 이유 때문에 esbuild-loader는 확연히 빠른 속도를 보여준다.

 

다만, esbuild-loader의 경우 타입 체킹이 되지 않는다.

ts-loader로 실행했을 때는 바로 타입 오류를 캐치하지만

 

 

esbuild-loader의 경우 아무 문제 없이 실행된다.

따라서 esbuild-loader에서 타입 체킹을 하고 싶다면 IDE의 도움을 받거나 fork-ts-checker-webpack-plugin 등을 따로 사용해야 한다.

 

 


🐶 완료 후 점수

 


🐶 배포 후 사이즈

 


🐶 프랑스 파리에서 Fast 3G 환경으로 접속

 

댓글