개발서적 스터디를 시작했는데 1장 발표자가 되었다. 한 번 날려먹어서 두 번 정리한 블로그 서비스 최적화 내용. 거의 복사 붙여넣기 수준으로 적었지만 타이핑을 하다보니 나름대로 좀 더 이해가 잘 되었다.
분석 툴 소개
- 크롬 개발자 도구의 Network 패널: 현재 웹 페이지에서 발생하는 모든 네트워크 트래픽을 상세하게 알려줌
- 크롬 개발자 도구의 Performance 패널: 웹 페이지가 로드될 때 실행되는 모든 작업을 보여줌. 이 패널을 통해 어떤 자바스크립트 코드가 느린지 확인할 수 있음
- 크롬 개발자 도구의 LightHouse 패널: 웹사이트의 성능 점수를 측정, 개선 가이드 확인 가능
- webpack-bundle-analyzer: npm을 통해 설치. 완성된 번들 파일 중 불필요한 코드가 어떤 코드인지, 번들 파일에서 차지하는 비중 확인 가능
서비스 탐색 및 코드 분석
- 깃허브 주소, 서비스 실행 방법, 코드 분석하는 내용... 중략함
Lighthouse 툴을 이용한 페이지 검사
Mode
- Navigation: 기본값. 초기 페이지 로딩 시 발생하는 성능 문제를 분석
- Timespan: 사용자가 정의한 시간 동안 발생한 성능 문제 분석
- Snapshot: 현재 상태의 성능 문제 분석
Categories
- Performance: 웹 페이지의 로딩 과정에서 발생하는 성능 문제를 분석
- Accessibility: 서비스의 사용자 접근성 문제 분석
- Best practices: 웹사이트의 보안 측면과 웹 개발의 최신 표준에 중점을 두고 분석
- SEO: 검색 엔진에서 얼마나 잘 크롤링되고 검색 결과에 표시되는지 분석
- Progressive Web App: 서비스 워커와 오프라인 동작 등, PWA와 관련된 문제 분석
Web Vitals - 웹 페이지의 성능을 나타내는 지표(metrics)
- First Contentful Paint (FCP): 페이지가 로드될 때 브라우저가 DOM콘텐츠의 첫번째 부분을 렌더링하는데 걸리는 시간.
- Speed Index (SI): 페이지 로드 중 콘텐츠가 시각적으로 표시되는 속도를 나타내는 지표. A와 B페이지가 각각 로드될 때 동일하게 4초가 걸렸더라도, 일부 콘텐츠가 먼저 로드되기 시작한 페이지가 더 빨리 로드된 것으로 계산되어 더 높은 점수를 받는다.
- Largest Contentful Paint (LCP): 페이지가 로드될 때 화면 내 가장 큰 이미지나 텍스트요소가 렌더링되기까지 걸리는 시간.
- Time to Interactive (TTI): 사용자가 페이지와 상호 작용(클릭 또는 키보드 누름)이 가능한 시점까지 걸리는 시간. 이 시점 전까지는 화면이 보이더라도 사용자 입력이 동작하지 않는다.
- Total Blocking Time (TBT): 페이지가 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표. FCP와 TTI사이 시간에 일어나며, 메인스레드를 독점하여 다른 동작을 방해하는 작업에 걸린 시간을 총합한다.
- Cumulative Layout Shift (CLS): 페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동을 측정한 지표. 화면상에서 요소의 위치나 크기가 순간적으로 변하는 것.
그 외 참고 항목
- Opportunities: 페이지를 더 빨리 로드하는 데 잠재적으로 도움되는 제안을 나열함
- Diagnostics: 로드 속도와 직접적인 관계는 없지만 성능과 관련된 기타 정보를 보여줌
- Emulated Desktop: CPU throttling 정보 확인 가능. CPU성능을 어느정도 제한하여 검사를 진행했는지 나타낸다. 1x라고 표시되면 제한을 두지 않고 검사했다는 뜻이다.
- Custom throttling: Network throttling 정보 확인 가능. 네트워크 속도를 제한하여 어느정도 고정된 네트워크 환경에서 성능을 측정했다는 의미다.
이미지 사이즈 최적화
- 비효율적인 이미지 분석. Proper size images
- 120x120으로 표시되는 이미지를 1200x1200으로 가져오고 있는데 이를 240x240정도로 사용해주면 최적화할수 있다.
- 이미지가 정적 이미지라면 직접 이미지 사이즈를 조절하면 되는데, API를 통해 받아오는 경우에는 이미지 CDN을 사용하여 이미지 사이즈를 최적화할 수 있다.
- CDN이란? 물리적 거리의 한계를 극복하기 위해 소비자와 가까운 곳에 콘텐츠 서버를 두는 기술이다.
- 이미지 CDN은 기본적인 CDN기능에 더해 이미지 사이즈를 줄이거나 특정 포맷으로 변경하는 등 이미지를 사용자에게 보내기 전에 가공하여 전해주는 기능이 있다. ex; Cloudinary, Imgix
병목 코드 최적화
Performance 패널 살펴보기
- Diagnotics 섹션의 Reduce JavaScript execution time 항목.
Performance 패널 사용법 두가지
1. View Original Trace 버튼 -> Performance 패널로 이동: Lighthouse를 통해 분석한 내용을 Performance 패널로 가져가서 보여준다.
2. 직접 Performance 패널로 이동: Performance패널로 이동한 뒤 새로고침 버튼을 누르면 된다.
분석 결과 화면 보기
1. CPU 차트, Network 차트, 스크린샷
- CPU차트: 시간에 따라 CPU가 어떤 작업에 리소스를 활용하는지 비율로 보여줌. 차트 위의 빨간색 선은 병목이 발생하는 지점을 의미한다. 특정 작업이 메인 스레드를 오래 잡아두고 있다는 뜻.
- Network 차트: CPU차트 밑에 막대 형태로 표시된다. 위쪽의 진한 막대는 우선순위가 높은 네트워크 리소스를, 아래쪽의 옅은 막대는 우선순위가 낮은 네트워크 리소스를 나타낸다.
- 스크린샷 리스트: 서비스가 로드되는 과정을 보여준다
2. Network 타임라인
- 서비스 로드 과정에서의 네트워크 요청을 시간 순서에 따라 보여준다.
3. Frames, Timings, Main
- Frames: 화면의 변화가 있을 때마다 스크린샷을 찍어 보여줌
- Timings: User Timing API를 통해 기록된 정보 표시. 막대들은 리액트에서 각 컴포넌트의 렌더링 시간을 측정한 것이다. (*리액트의 User Timing API 코드는 리액트 버전 17이후로 정확성 및 유지보수 문제로 지원이 종료되었다. 참고만 할 것)
- Main: 브라우저의 메인 스레드에서 실행되는 작업을 플레임 차트로 보여줌. 어떤 작업이 오래 걸리는지 파악할 수 있다.
4. 하단 탭
- Summary: 선택 영역에서 발생한 작업 시간의 총합과 각 작업이 차지하는 비중을 보여줌
- Bottom-up: 최하위 작업부터 상위 작업까지 역순으로 보여줌
- Call Tree: 반대로, 상위 작업부터 하우이 작업 순으로 작업 내용을 트리 뷰로 보여줌
- Event Log: 발생한 이벤트를 보여줌. 이벤트에는 Loading, Experience scripting, Rendering, Painting이 있다.
페이지 로드 과정 살펴보기
페이지가 처음 로드되는 시점에서 파란색으로 보이는 localhost 라는 네트워크 요청은 HTML파일에 대한 요청을 의미한다. 이어서 주황색 막대를 보면 *.js파일들을 로드하고 있다. 로드 시간이 매우 긴 .js파일을 클릭하고 Summary 탭을 보면 파일 크기가 4.2MB로 매우 크다는 것을 알 수 있다. 여기서는 체크만 하고 넘어감.
HTML이 다운로드된 시점을 보면 메인 스레드에서 Parse HTML이라는 작업을 하고 있다. 네트워크를 통해 받은 HTML을 처리하는 것으로 추정된다.
0.chunk.js의 다운로드가 끝난 시점을 보면 이어서 메인 스레드에서 자바스크립트 작업이 실행되고 있다. Timings 섹션의 바 항목에 커서를 올리면 간단한 정보가 뜨는데, 여기서 실행 시간을 확인할 수 있다. 렌더링하는 데 걸리는 시간. 시간이 1.4초로 오래 걸리는 항목의 구간을 따라 내려가다 보면 여러가지 작업이 보인다. 'Minor CG'라는 작업은 자바스크립트의 가비지 컬렉션 작업이므로 실제 코드와 관계없으니 무시해도 된다.
-> 의문사항... Article이라는 작업은 Article 컴포넌트를 렌더링하는 작업으로 보인다고 하는데 그 아래에 removeSpecialCharacter이라는 작업이 캡쳐상으로 8.48ms 라고 뜨는데 이게 시간이 오래 걸렸다는 걸 어떻게 알지?
removeSpecialCharacter 최적화 방안
- 특수문자를 효율적으로 제거하기: substring과 concat대신 replace함수를 사용
- 작업량 줄이기: 문자열 90,021자를 전부 반복문 돌지 않고 보여줘야 하는 200자 정도만 잘라서 탐색하는 것으로 변경
최적화 전후 비교
최적화 전에는 1.4초 걸리던 작업이 최적화 후에는 36밀리초로 줄어들었다.
코드 분할 & 지연 로딩
번들 파일 분석
webpack을 통해 번들링된 파일을 분석하고 최적화해보자.
유난히 크고 다운로드가 오래 걸렸던 자바스크립트 파일, 0.chunk.js 파일이 있다. 최적화를 하려면 먼저 cra-bundle-analyzer를 npm으로 설치한다. 실행해보면 번들 분석 결과가 뜬다. 파일의 실제 크기에 따라 비율로 보여주기 때문에 어떤 패키지가 어느 정도의 용량을 차지하고 있는지도 쉽게 알 수 있다.
가장 많은 부분을 차지하고 있는 2.chunk.js파일이 어떤 패키지때문에 큰 것인지 확인해보자. refractor와 react-dom이 큰 비중을 차지하는데, react-dom은 리액트를 위한 코드이므로 넘어가고 refractor패키지의 출처를 확인해보자. 패키지 출처는 package-lock.json 또는 yarn.lock에 명시되어 있다. 이 파일에서 refractor패키지에 대한 내용을 찾아보고, 이 패키지의 기능을 알아보자. refractor에 의존하는 react-syntac-highlighter 라이브러리는 CodeBlock.js에서 사용하고 있는데, 이것은 상세페이지에서만 필요하고 목록페이지에서는 필요가 없다. 그러니 이 번들 파일을 페이지별로 필요한 내용만 분리하여 필요할 때 따로따로 로드하면 좋을 것 같다.
코드 분할이란
Code Splitting 기법을 이용해서 페이지별로 코드를 분리한다. 하나의 번들 파일을 여러개의 파일로 쪼개는 방법이다. 분할된 코드는 사용자가 서비스를 이용하는 중 해당 코드가 필요해지는 시점에 로드되어 실행되는데, 이것을 지연 로딩이라고 한다.
코드 분할 기법에는 여러가지 패턴이 있다. 페이지별로 분할할수도 있고 여러 페이지에서 공통으로 쓰이는 모듈이 많고 사이즈가 크면 모듈별로 분할할 수도 있다. 핵심은 '불필요한 코드 또는 중복되는 코드 없이 적절한 사이즈의 코드가 적절한 타이밍에 로드되도록 하는 것'이다.
코드 분할 적용하기
아래와 같이 import문을 사용하면 빌드할 때가 아닌 런타임에 해당 모듈을 로드한다.
// import문을 이용한 동적 import
import('add').then((module) => {
const { add } = module
console.log('1+4=', add(1,4))
})
webpack은 이 동적 import구문을 만나면 코드를 분할하여 번들링하는데, 이것은 Promise 형태로 모듈을 반환해 주기 때문에 Promise 내부에서 로드된 컴포넌트를 Promise 밖으로 빼내야 한다. 다행히 리액트는 lazy와 Suspense를 제공하여 이 문제를 해결해준다.
import React, { Suspense } from 'react'
const SomeComponent = React.lazy(() => import('./SomeComponent'))
function MyComponent () {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<SomeComponent/>
</Suspense>
</div>
)
}
lazy함수는 동적 import를 호출하여 그 결과인 Promise를 반환하는 함수를 인자로 받는다. lazy함수가 반환한 값, 즉 import한 컴포넌트는 Suspense 안에서 렌더링해야 한다. 동적 import를 하는 동안 SomeComponent가 아직 값이 없을 때는 Suspense의 fallback prop에 정의된 내용으로 렌더링되고, 이후 값이 로드되면 정상적으로 SomeComponent가 렌더링된다.
페이지별로 코드를 분할할 예정이면 Router쪽에 이 코드를 적용해야 한다.
(예시 생략)
Router에 코드를 적용한 뒤 Performance 패널을 확인해 보면, 전에는 4.2MB에 6.3초정도 걸렸던 chunk 파일이 코드 분할 후 대략 1.9MB에 3초정도로 줄어든 것을 볼 수 있다. Network throttling의 설정 상황과 production 빌드를 통하면 더 많은 부분 감소할 것이다.
텍스트 압축
production 환경과 development 환경
블로그 서비스가 실행되고 있는 환경이 prod 환경이 아니고 dev환경인 점을 짚고 넘어가야 한다. create-react-app인 경우 prod와 dev환경에 차이가 있기 때문이다. 환경에서 성능을 측정할 때 차이가 있으므로 최종 서비스 성능을 측정할 때는 prod환경으로 빌드된 서비스의 성능을 측정해야 한다.
빌드 전후 서비스를 비교해보면, 가장 큰 chunk파일이 436kB에서 156kB로 줄어들었다. 빌드할 때 경량화 같은 최적화가 이루어졌기 때문이다.
Lighthouse로 prod환경의 상세페이지를 검사하니까 점수가 많이 낮다. 큰 패키지가 번들파일에 포함되어 있고 글 내용도 모두 들어있기 때문에 성능 점수가 목록 페이지에 비해 낮을 수 있다. 그렇지만 Opportunities 섹션의 'Enable text compression'을 짚고 넘어가야 한다. 이 항목은 '서버로부터 리소스를 받을 때, 텍스트 압축을 해서 받아라'라는 의미다. 예상치를 보면 444KiB인 청크 파일을 268KiB정도 줄이고, 다운로드 시간을 0.3초정도 단축시킬 수 있다고 한다. 텍스트 압축(text compression)이라는 최적화 기법에 대해 알아보자.
텍스트 압축 기법은 리소스의 크기를 줄이는 기법 중 하나이다. 기본적으로 HTML, CSS, 자바스크립트는 텍스트 기반의 파일인데, 이런 파일들을 압축하여 더 작은 크기로 빠르게 전송한 뒤 사용하는 시점에 압축을 해제한다. 압축 여부를 확인하려면 HTTP의 헤더를 살펴보면 된다. 응답 헤더(Response Header)에 'Content-Encoding: gzip'이라고 되어있으면 리소스가 gzip이라는 방식으로 압축되어 전송되었다는 의미다.
웹에서 사용하는 압축 방식에는 크게 Gzip, Deflate의 두 가지가 있다.
그에 반해 main 번들 파일을 확인해 보면 응답 헤더에 'Content-Encoding'이라는 항목이 없다. 텍스트 압축이 적용되어 있지 않다는 뜻이고, 이런 파일에 텍스트 압축을 적용할 예정이다.
텍스트 압축 적용
텍스트 압축은 리소스를 제공하는 서버에서 설정해야 한다. 스크립트의 serve 명령어를 살펴보면 -u, -s옵션이 붙은 것을 볼 수 있다. 이 옵션의 내용을 확인해보면 s옵션은 SPA 서비스를 위해 매칭되지 않는 주소는 모두 index.html로 보내겠다는 옵션이고, u옵션은 텍스트 압축을 하지 않겠다는 옵션이다. 텍스트 압축을 적용하기 위해서는 u옵션만 제거하면 된다.
서버에 따라 텍스트 압축 설정 방법이 달라질 수 있다. 단일 서버가 아닌 여러 서버를 사용하고 있다면 Nginx와 같은 게이트웨이 서버에 공통적으로 텍스트 압축을 적용할 수도 있다.
'TIL > 읽을거리' 카테고리의 다른 글
Un fixing fixed Elements with CSS transforms (0) | 2023.07.26 |
---|---|
vim (0) | 2023.06.17 |
HTTP 상태 코드 정리된 웹사이트 (0) | 2023.06.01 |
Javascript는 싱글 스레드 언어인가? (0) | 2023.03.26 |
Global Styles (0) | 2023.02.20 |