본문 바로가기

Frontend/성능 최적화

[블로그 병목 코드 최적화] 웹 성능 최적화까지 해보자-3

병목 코드 최적화

이번에는 Diagnostics 섹션Reduce JavaScript execution time 항목을 살펴보자.

0.chunk.js 파일에서 1163ms동안 자바스크립트가 실행되었음을 알 수 있다.

 

Performance 패널을 활용해서 느린 작업이 무엇인지 확인해보자.

Lighthouse 결과 페이지에서 ‘View Original Trace’ 버튼을 눌러서 Performance 패널로 이동해보자.

그러면 Lighthouse를 통해 분석한 내용을 Performance 패널로 가져가서 보여준다.

 

먼저 CPU 차트, Network 차트, 스크린샷을 살펴보자.

 

CPU 차트는 시간에 따라 CPU가 어떤 작업에 리소스를 사용하고 있는지 비율로 보여준다.

이 차트를 통해서 어느 타이밍에 어떤 작업이 주로 진행되고 있는지 파악할 수 있다.

그리고 그 위 빨간색 선은 병목이 발생하는 지점을 의미한다.

즉, 특정 작업이 메인 스레드를 오랫동안 잡아 두고 있다는 뜻이다.

 

Network 차트는 CPU 차트 밑에 막대 형태로 표시된다.

대략적인 네트워크 상태를 보여주는데, 진한 막대의 경우 우선순위가 높은 네트워크 리소스를, 옅은 막대는 우선순위가 낮은 네트워크 리소스를 나타낸다.

그 아래 스크린샷 리스트는 서비스가 로드되는 과정을 보여준다.

그 다음으로, Network 타임라인을 한 번 살펴보자.

Network 타임라인은 Network 패널과 유사하게 서비스 로드 과정에서의 네트워크 요청을 시간 순서에 따라 보여준다.

각 네트워크 요청 막대에서 왼쪽 회색 선은 초기 연결 시간, 막대의 옅은 색 영역은 요청을 보낸 시점부터 응답을 기다리는 시점까지의 시간(TTFB, Time to First Byte), 막대의 짙은 색 영역은 콘텐츠 다운로드 시간, 오른쪽 회색 선은 해당 요청에 대한 메인 스레드의 작업 시간을 나타낸다.

 

다음으로, Frames, Timings, Main에 대해서 살펴보자.

Frames 섹션은 화면의 변화가 있을 때마다 스크린샷을 찍어 보여준다.

 

Timings 섹션은 User Timing API를 통해 기록된 정보를 기록한다.

표시된 막대들은 리액트에서 각 컴포넌트의 렌더링 시간을 측정한 것이다.

 

Main 섹션은 브라우저의 메인 스레드에서 실행되는 작업을 플레임 차트로 보여주는데, 이를 통해서 어떤 작업이 오래 걸리는지 파악할 수 있다.

 

마지막으로, 하단 탭도 살펴보자.

하단에 있는 Summary, Bottom-Up, Call Tree, Event Log 탭에서는 전체 또는 선택된 영역에 대한 상세 내용을 확인할 수 있다.

Summary 탭은 선택 영역에서 발생한 작업 시간의 총합각 작업이 차지하는 비중을 보여준다.

 

Bottom-Up 탭은 가장 최하위에 있는 작업부터 상위 작업까지 역순으로 보여준다.

 

Call Tree 탭은 Bottom-Up과 반대로 가장 상위 작업부터 하위 작업 순으로 작업 내용을 트리뷰로 보여준다.

 

Event Log 탭은 발생한 이벤트를 보여준다. 이벤트로는 Loading, Experience, Scripting, Rendering, Painting이 있다.

 

페이지 로드 과정 살펴보기

처음에는 localhost라는 네트워크 요청이 파란색으로 보인다.

이 막대는 HTML 파일에 대한 요청을 의미한다.

이후, bundle.js, 0.chunk.js, main.chunk.js 파일을 로드하고 있다.

황색 막대는 자바스크립트 파일에 대한 요청을 의미한다.

 

하지만, 막대를 보면 알 수 있다시피 0.chunk.js의 로드 시간이 매우 길다.

이 부분은 나중에 꼭 수정해야 할 부분이다.

 

HTML 파일이 다운로드된 시점을 보면 메인 스레드에서는 ‘Parse HTML’ 작업을 하고 있다.

이는 네트워크를 통해 받은 HTML을 처리하고 있는 것이다.

 

0.chunk.js의 다운로드가 끝난 시점에는 자바스크립트 작업이 실행된다.

 

이제 Timings 섹션을 살펴보자.

메인 스레드의 자바스크립트 작업이 끝나는 시점에 컴포넌트에 대한 렌더링(App[mount]) 작업이 기록되어 있다.

 

컴포넌트가 마운트되면 ArticleList 컴포넌트에서는 블로그 글 데이터를 네트워크를 통해 요청하는데, 그 정보가 Network 섹션에 ‘articles (localhost)’라는 이름으로 기록되어 있다.

articles 데이터가 모두 다운로드되니 메인 스레드에서는 해당 컴포넌트를 렌더링하기 위해 자바스크립트를 실행한다.

 

그런데, Timings 섹션의 ArticleList 항목에 커서를 올려두면 약 0.9초 동안 실행된다고 뜬다.

이는 네트워크 시간을 포함한 시간이 아니라 모든 데이터가 준비된 상태에서 단순히 데이터를 화면에 그리는 것(렌더링)일 뿐인데 생각보다 오래 걸린다.

자세히 살펴보자.

 

removeSpecialCharacter 작업이 Article 컴포넌트의 렌더링 시간을 길어지게 하는 것으로 밝혀졌다.

이는 src/Article/index.js 파일 내부 함수 이름이다.

이 함수를 최적화하면 ArticleList의 실행 시간도 단축될 것이다.

 

병목 코드 개선

최적화 이전 removeSpecialCharacter 함수는 다음과 같다.

이 함수는 인자로 넘어온 문자열에서 특수 문자를 제거하는 함수이다.

마크다운으로 된 블로그 글에서 특수 문자를 모두 지우고 본문 일부를 보여주기 위해서 사용된다.

function removeSpecialCharacter(str) {
  const removeCharacters = ['#', '_', '*', '~', '&', ';', '!', '[', ']', '`', '>', '\n', '=', '-']
  let _str = str
  let i = 0,
    j = 0

  for (i = 0; i < removeCharacters.length; i++) {
    j = 0
    while (j < _str.length) {
      if (_str[j] === removeCharacters[i]) {
        _str = _str.substring(0, j).concat(_str.substring(j + 1))
        continue
      }
      j++
    }
  }

  return _str
}

해당 함수의 로직을 자세히 살펴보자.

 

removeCharacters라는 이름으로 제거할 특수 문자를 정의해두고, 각 특수 문자마다 반복문을 돌려 본문에 일치하는 내용을 탐색하고 제거한다.

하지만, 이를 위해 반복문을 두 번 중첩해서 사용하고 있고, 문자열을 제거할 때도 substring과 concat 함수를 사용하는데, 이는 일치하는 문자를 찾아서 제거해주는 replace 함수로 대체하면 비효율적인 코드를 없앨 수 있다.

 

또한, 서비스에 사용되는 것은 블로그 글 데이터 중 앞의 300자 정도만 잘라서 특수 문자를 제거해주면 되기 때문에 이를 이용하면 조금 더 최적화해볼 수 있다.

 

최적화 후의 removeSpecialCharacter 함수는 다음과 같다.

function removeSpecialCharacter(str) {
  let _str = str.substring(0, 300);
  _str = _str.replace(/[#_*~&;![\\]`>\\n=\\->]/g, '');

  return _str;
}

 

최적화 전후 비교

최적화 후 작업 시간이 정말 많이 줄어든 것을 확인할 수 있다.

최적화 전에는 ArticleList 컴포넌트 렌더링하는데 899ms가 걸린 반면, 최적화 후에는 11ms밖에 걸리지 않았다.

 

이번에는 Lighthouse로도 검사해보자.

이전보다 점수가 많이 올랐다.

그리고 Total Blocking Time도 최적화 전에 비해 많이 줄어든 것을 확인할 수 있다.

Diagnostics 섹션에서 Reduce JavaScript execution time 항목도 사라진 것을 확인할 수 있다.