owned this note
owned this note
Published
Linked with GitHub
# 웹 성능 최적화
###### tags: `FE`
페이지 로딩 시간은 모바일 사용자의 UX에 큰 영향을 주므로 모바일 기기 성능을 우선시로 사이트 디자인을 최적화해야 한다.
최적화 항목
- 페이지에서 로딩하는 자원의 수(이미지, 글꼴, HTML, CSS)
- 로딩한 자원 파일의 크기
- 사용자가 느끼는 사이트의 체감 성능
브라우저는 웹 페이지를 렌더링하는 시간을 줄이기 위해 서버에 보내는 콘텐츠 요청을 병렬로 처리한다.
브라우저가 파일 요청하는 동시 연결의 수는 최대 6~8개이다.
컨텐츠의 파일 크기를 종류별로 측정하는 브라우저 플러그인 YSlow
## 렌더링 과정
### 1. Dom Tree 생성
- HTML 파일을 받으면 Parsing과 Lexing 과정을 거쳐 Dom Tree로 만든다.
- 바이트를 문자로 만든 뒤, 문자열을`<body>`같은 토큰으로 만드는 작업을 거친다.
- 이를 통해 페이지를 표시하기 위해 모든 페이지 작업에 필요한 DOM 트리를 만든다.
- HTML을 파싱하는 과정에서 스타일시트를 만나면 모든 작업을 일시 중지하고 서버에 파일을 요청한다.
### 2. CSSOM 생성
- 서버에서 받은 스타일시트 파일도 Tokenizer, Lexing을 거쳐 CSSOM을 생성한다.
### 3. Style(Render Tree 생성)
- 만들어진 DOM과 CSSOM을 사용해 스타일을 매칭시켜 Render Tree를 구성한다.
- Render Tree는 화면에 보이는 모든 요소의 위치와 크기를 계산하는데 사용한다.
- 렌더링 트리는 페이지를 표시하는데 필요한 정보만 가지고 있다. 그래서 `display: none` 같은 것은 렌더링 트리에 포함하지 않는다.
### 4. Layout
- Render Tree 노드의 정확한 위치와 크기를 계산한다.
- 노드의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하며, 이 과정으로 나온 위치와 크기를 픽셀 값을 Render Tree에 반영한다.
- 예를 들어, `%`로 지정한 값은 Layout 과정을 거치면 `px`단위로 바뀐다.
### 5. Paint
- 위치와 관계없는 색상, 투명도 같은 속성을 적용한다.
- 픽셀로 변환된 결과는 개별 레이어로 관리되는데, `transform` 속성 등을 사용하면 요소가 레이어화된다.
- 이렇게 실제 픽셀 값을 사용해 레이어들을 만드는 것을 Paint 과정이라고 한다.
### 6. Render
- Paint 단계에서 생성된 레이어를 합성해 스크린을 업데이트한다.
## Reflow와 Repaint
### Reflow
- DOM과 CSSOM을 사용해 Style, Layout, Paint, Render를 거치는 과정을 렌더링이라고 한다.
- Render Tree는 JS에 의해 DOM이나 CSSOM 노드에 높이, 넓이, 위치 등의 CSS 속성이 변경되는 경우 재구성된다.
- 전체 픽셀을 다시 계산해야 하기 때문에 부하가 커서 지양해야 한다.
- 즉, 전체 픽셀을 다시 계산하는 Layout 다음 Paint 과정을 거쳐 성능에 영향을 준다.
- height, width, left, top, font-size, line-height이 해당된다.

### Repaint
- 높이, 넓이, 위치 등의 기하하적 영향이 없는 경우 Layout 과정 없이 Paint가 일어난다.
- 즉, 크기와 위치를 계산하는 시간이 없어졌기 때문에 Reflow가 아닌 대안으로 사용하길 지향한다.
- background-color, color, visibility, text-decoration이 이에 해당된다.

## Layout(Reflow) 최적화
- Layout을 최대한 적게하고 Repaint만 할 수 있도록 최적화하는 방법
### 1. 강제 동기 레이아웃 최적화
- 원래 레이아웃은 비동기로 일어나지만 특정 상황에선 동기적으로 일어난다.
- 특정 속성을 읽을 때 최신 값을 계산하기 위해 레이아웃은 동기적으로 발생한다.
- `offsetHeight`, `offsetTop` 속성을 접근하면 값을 계산해야 하기 때문에 강제로 동기 레이아웃이 실행된다.
### 2. 레이아웃 스래싱 피하기
- `offsetHeight` 같은 속성을 많이 호출하는 것을 없애는 것
```javascript=
function resizeAllParagraphs() {
const box = document.getElementById('box');
const paragraphs = document.querySelectorAll('.paragraph');
for (let i = 0; i < paragraphs.length; i += 1) {
paragraphs[i].style.width = box.offsetWidth + 'px'; // 이 부분이 스래싱이 걸린다.
}
}
// ===> 최적화 후
function resizeAllParagraphs() {
const box = document.getElementById('box');
const paragraphs = document.querySelectorAll('.paragraph');
const width = box.offsetWidth; // 한번의 Layout만 수행시키고 값을 변수에 저장
for (let i = 0; i < paragraphs.length; i += 1) {
paragraphs[i].style.width = width + 'px';
}
}
```
### 3. 가능한 최소 수의 하위 DOM 노드를 조작하게 함
- DOM 트리의 상위 노드에 Layout이 생기면 하위 노드에 모두 영향을 준다.
- 그래서 변경 범위를 최소화 하면 Layout 범위를 줄일 수 있다.
- 부모 요소의 높이가 가변적일 때, 자식 요소의 높이가 변경되면 부모 요소의 높이가 Layout 되고 이는, 부모의 형제 요소들에게도 영향을 준다.
- 이럴 경우 부모 요소의 높이를 고정시켜 Layout을 최소화 시킨다.
- 여러 개의 Inline 요소들이 한 줄에 놓여있으면, 하나의 width 변경이 다른 요소들에게 영향을 준다.
### 4. 요소를 숨긴 상태에서 수정하라.
- `display: none`의 요소는 DOM 조작이나 스타일을 변경해도 Layout과 Repaint을 발생시키지 않는다.
- 따라서 많은 수의 요소를 변경해야 하면 `display: none` 상태에서 요소를 변경하고 다시 보이게 해 Layout을 줄인다.
- `visibility: hidden`은 Repaint가 발생하지 않지만, 공간을 차지하기 때문에 Layout이 발생한다.
### 5. CSS 규칙 수를 최소화한다.
- 요소의 class가 변경되면 렌더링이 발생하므로 CSS가 복잡하면 안된다.
- 사용하는 규칙이 적을수록 계산이 빨라진다.
- 복잡한 선택자는 스타일 계산에 시간이 많이 걸려 최소화 시킨다.
### 6. DOM 노드 수를 줄이자.
- DOM 트리가 깊을수록, 하나의 노드에 자식 노드가 많을 수록 DOM이 커진다.
- 이 말은 즉, DOM을 갱신할 때 수행할 계산이 많아진다는 것
- 불필요한 Wrapper 요소들을 제거한다.
### 7. 애니메이션 최적화
- 프레임 처리 속도는 16ms(60fps)가 되야 자연스럽다.
- 따라서 JS 실행 시간을 10ms 내에 수행시켜야 60fps를 완료시킬 수 있다.
- 자바스크립트 API 보다 CSS가 빠르다.(애니메이션 최적화, GPU 사용)
- `requestAnimationFrame`
- 브라우저의 프레임 속도(주로 60fps)에 맞춰 애니메이션을 실행할 수 있게 한다.
- setInterval, setTimeout과 다르게 프레임을 시작할 때 호출되기 때문에 일정한 간격으로 애니메이션을 수행할 수 있다.
- 또한, 페이지가 보이지 않을 경우 콜백함수가 호출되지 않기 때문에 불필요한 동작을 하지 않는다.
- `position: absoulte`로 애니메이션 영역이 주변 영역에 영향을 주지 않도록 할 수 있다.
- `transform` 사용
- position, width, height 같은 기하적 변화를 야기하는 속성은에 적용하면 Layer로 분리되기 때문에 영향을 주는 요소가 제한되어 Layout과 Repaint를 줄일 수 있다.
- 또한, 합성만 발생시키기 때문에 애니메이션 사용 시, 렌더링 속도가 향상된다.
- left, top 같은 속성을 사용하면 모든 프레임마다 요소와 배경이 합성되어 시간이 더 걸리기 때문에, `transform: translate()`를 사용해야 한다.
```css=
.animation-item {
position: absolute; /* absolute */
top: 0;
left: 0;
width: 50px;
height: 50px;
background-color: red;
animation: move 3s ease infinite;
}
@keyframes move {
50% {
transform: translate(100px, 100px);
}
}
```
## Block Resource
- HTML 파싱이 일어날 때 CSS나 JS에 의해 파싱이 중단될 수 있다.
- 중단을 일으킨 리소스를 Block Resource 라고 한다.
- Block Resource는 Paint 과정을 지연시키기 때문에 주요 렌더링 경로 최적화 등으로 페인팅을 빠르게 만들어 로딩 속도를 개선해야 한다.
### Block Resource 최적화
#### CSS
- Render Tree를 위해선 CSSOM의 생성이 완료되어야 한다.
- 따라서, 렌더링이 차단되지 않도록 항상 HTML 문서의 최상단에 배치한다.
- HTML Parsing과 CSSOM parsing이 병렬로 이루어질 수 있기 때문에, 스타일 시트를 가장 먼저 불러온 뒤, Tree 생성을 같이 수행하게 한다.
- 또한, 특정 조건에만 사용하는 CSS는 미디어 쿼리를 사용해 불 필요한 블로킹을 방지할 수 있다.
- CSS 파일 내의 `@import`는 그 스타일 시트를 병렬로 다운로드할 수 없기 때문에 로드 시간이 늘어나 무조건 지양한다.
#### JS
- 외부에서 가져온 스크립트는 다운로드하고 실행될 때까지 DOM 생성을 중지한다.
- 그리고 스크립트 실행은 HTML Parsing을 중단시키기 때문에 Parsing이 다 끝난 후 스크립트를 실행하는 것이 좋다.
#### 요청 리소스 수 줄이기
- 대체로 파일의 다운로드 시간보다 대기 시간이 더 길다.
- 그래서 파일 요청을 줄이는 것이 성능상 중요하다.
- 방법
1. 이미지 스프라이트
- 같은 웹 페이지에서 사용하는 이미지들을 하나의 파일로 묶는다.
2. CSS와 JS 번들
- 번들러를 사용해 여러 모듈 파일을 하나로 묶어 1개로 만든다.
3. 작은 이미지를 HTML, CSS로 대체
4. Internal Style 적용
5. 리소스 용량 줄이기
#### 리소스 용량 줄이기
- 중복 코드 제거하기
- lodash같은 유틸 라이브러리를 일반적으로 사용하면 유틸 함수 전체가 포함될 수 있다.
```javascript=
import _ from 'lodash';
_.array(...);
_.object(...);
// ===>
import array from 'lodash/array';
import object from 'lodash/fp/object';
array(...);
object(...);
```
- HTML 마크업 최적화
- 태그의 중첩을 최소화
- 불필요한 태그, 공백, 주석 최소화
- 간결한 CSS 선택자 사용
- webpack 플러그인 같은 도구를 사용해 압축
## 성능 측정 지표
### 브라우저 기준의 측정

- `DOMContentLoaded`나 `load` 발생 시점이 빠를 수록 성능이 좋다.
- `DOMContentLoaded`나 `load` 발생 구간 폭이 좁을수록 성능이 좋다.
- DOMContentLoaded는 HTML과 CSS 파싱이 끝나는 시점에 발생하는 이벤트, Render Tree를 구성할 준비가 된 상황
- load는 HTML에 필요한 모든 리소스가 준비된 시점의 이벤트
- 하지만 SPA같은 패러다임으로, 적은 양의 HTML을 가져와 위의 이벤트들이 빠르게 동작할 수 있지만 사용자에게 느린 성능의 페이지를 제공할 수도 있다.
### 사용자 기준의 측정
- 완료 이벤트와 상관없이 웹 페이지의 렌더링 성능을 측정한다.
#### 주요 렌더링 경로


1. FP(First Paint)
- 흰 하면에서 무언가가 처음으로 그려지는 순간
2. FCP(First Contentful Paint)
- 텍스트나 이미지가 출력되기 시작한 순간
3. FMP(First Meaningful Paint)
- 사용자에게 의미 있는 콘텐츠가 그려지는 첫 순간
- 콘텐츠를 보여주는데 필요한 CSS, JS 로드가 시작되고 스타일이 적용되어 주요 콘텐츠를 읽을 수 있다.
4. TTI(Time To Interactive)
- JS의 초기 실행이 완료되어서 사용자가 직접 행동을 취할 수 있는 순간
## 성능 측정 도구
- 크롬의 개발자 도구 패널 Network, Performance, Audits가 있다.
### 1. Performance 패널

- 웹 페이지 로드 과정을 레코딩하고 그 단계마다 시간을 확인할 수 있다.
### 2. Network 패널

- 웹 페이지 로드 과정에서 서버에게 요청된 리소스의 상태를 확인할 수 있다.
#### 서버 요청 대기 시간

1. Queuing
- 대기열에 쌓아둔 시간
- JS, CSS보다 우선순위가 낮아 대기한다.
- TCP 소켓 대기, TCP 연결 초과 대기, 디스크 캐시 항목 작성 소요 시간
2. Stalled
- 요청을 보내기 전의 대기 시간
- Queuing에서 쌓인 대기 시간 소모
- 프록시 협상에 드는 시간
3. DNS Lookup
- DNS 조회에 소비된 시간
4. Initial Connection
- TCP HandShake, 재시도 및 SSL 연결에 걸리는 시간
5. Waiting(TTFB)
- 초기 응답을 기다리는데 소비한 시간(서버 왕복 시간)
6. Content Download
- 리소스 실제 다운로드 시간
### 3. Audits 패널

사용자 기준의 성능 측정 지표를 확인할 수 있다.
1. Metrics
- 다양한 성능 측정 지표를 보여줌
- FCP, FMP, TTI 시점을 확인할 수 있다.
- 웹 페이지가 화면에 그려지는 단계를 스크린샷으로 보여준다.
2. Opportunities
- 최적화 가능한 리소스 목록을 보여준다.
3. Diagnostics
- 리소스 최적화 외에 성능을 개선할 부분을 진단하고 해결 방안을 목록으로 보여줌
- 주요 렌더링 경로 최적화시, Critical Request Chains를 참조한다.
4. Passed audits
- 성능 측정 기준과 통과 목록을 보여준다.
<!--
Css는 렌더링을 멈추게 하는 자원 중 하나이므로 미디어 타입과 미디어 쿼리로 어떤 부분을 멈추지 않고 계속 렌더링할 것인지 표시한다.
- 따라서, 반응형 웹을 만들 경우 크기에 따라 css 파일을 분리한 뒤, link 태그에 media 타입을 지정하는 것이 좋다.
```htmlmixed=
<link rel="stylesheet" href="big-screens.css" media="(min-width: 1200px)"
```
- 비동기 방식으로 콘텐츠 가져오기
- 화면에 보이는 부분이ㅡ 콘텐츠를 먼저 표시하도록 우선순위 높이기
- CSS와 자바스크립트 로딩 모범 사례 따르기
- 재방문 사용자를 고려해 자원 캐싱하기
- 사용자가 페이지의 주요 기능을 최대한 빨리 사용할 수 있도록 보장하기
Jank(쟁크)
- 웹페이지를 스크롤할 때 화면이 버벅이거나 건너 뛰는 현상
- 사용자가 페이지에 포함된 요소의 시각적 속성에 영향을 미치는 행동을 할 때 브라우저가 새로 화면을 그리면서 발생한다.
## 이미지
- 각 이미지 파일의 크기와 품질 사이의 균형점 찾기
- 사이트의 이미지 요청 횟수 줄이기
- 사이트의 이미지 생성 워크플로어를 최적화해서 성능 개선하기
### JPEG
- 사진이나 많은 색이 사용된 이미지에 사용
- 해상도 줄이기, Progressive JPEG로 내보내기, 이미지 노이즈 줄이기
- 사람 눈으로 인식하는 방식으로 사람이 눈치채지 못할 정도로 정보를 손실시고 압축한다.
- 손실 압축 방식이므로 낮은 품질의 이미지를 저장하면 품질이 좋아 보이지 않는다.
- 인접 픽셀 간 명암 대비가 크면 PNG같은 다른 포맷이 더 적합할 수 있다.
- Progressive JPEG는 이미지 전체를 로딩하는 대신 낮은 품질로 즉시 표현해 기본 JPEG보다 화면에 더 빠르게 나타난다.
- 하지만 Progressive JPEG는 스캔할 때 페이지를 렌더링하기 위해 스캔하는 수준의 CPU를 사용한다.
### GIF
- 애니메이션에 사용
- dithering 줄이기, 색상 수 줄이기, 수평 패턴 사용, 수직 노이즈 줄이기
- dithering: 색상 간 전환이 더 부드럽게 보이게 도와주는 기능
- 색상이 다른 인접한 픽셀들을 검사해 부드러우면서 색상들이 조화롭게 보이도록 두 색 사이에 새로운 색을 골라 넣는다.
- 무손실 파일 포맷이다.
- 프레임 안에 256색까지만 포함하는 제한이 있다.
- 그레이디언트 방향을 수직으로 변경하고 디더링을 하면 파일의 크기가 더 커진다.
- 왜냐하면 GIF는 수평 중복성을 제거하는 압축 알고리즘을 사용하기 때문이다.
- 그래서 수직으로 된 내용이나 노이즈를 추가하면 파일의 크기가 증가한다.
- 그래서 수직으로 된 노이즈를 줄이면 GIF의 파일 크기에 긍정적인 도움이 된다.
- 투명성을 지원하지만 파일 크기가 대체로 PNG보다 훨씬 크다.
### PNG-8
- 적은 색이 사용된 이미지에 사용
- dithering 줄이기, 색상 수 줄이기, 수평 및 수직 패턴 사용
### PNG-24
- 일부 투명한 이미지에 사용
- 노이즈 줄이기, 색상 수 줄이기
p42
## 마크업
디비티스
- 콘텐츠에 스타일을 적용하는 것처럼 사소한 이유로 HTML 내에 과하게 많은 요소가 사용된 상태
### CSS
- 사용하지 않는 스타일을 제거하거나 합친다.
- 중복되는 스타일은 하나의 스타일로 합치고 차이나는 것만 별도로 작성한다.
- css에 사용되는 이미지는 스프라이트로 사용하면 효율적이다.
- 스타일 시트내 이미지를 CSS3 그레이디언트나 데이터 URI, SVG로 대체할 수 있는 곳을 찾는다.
- CSS3 그레이디언트는 반복 배경 이미지를 대체하기 좋다.
- 수정이 매우 쉬우며, 스타일 시트를 통해 재사용할 수도 있다.
- specificity) 제거
- specificity는 브라우저가 어떤 CSS 규칙을 적용할지 결정하는데 도움이 되도록 셀렉터를 작성하는 방법
- 많은 특이성을 사용했다는 것은 스타일시트나 HTML 계층구조에서 개선할 여지가 있다는 지표이다.
- 비효율적인 셀렉터와 !important는 CSS 파일을 커지게 하므로 지양하는 것이 좋다.
CSS는 `<head>`에서 로딩한다.
- 스타일시트를 페이지 하단 가까이에 두면 페이지의 렌더링을 방해한다.
- 브라우저가 렌더링할 콘텐츠의 스타일이 변경되고 있을 때 페이지의 요소를 화면에 다시 그리는 일을 피하려고 하기 때문이다.
- 하지만 `<head>`안에 넣으면 브라우저가 더 이상 스타일 정보를 찾지 않아 사용자에게 화면이 순차적으로 보이도록 할 수 있다.
- CSS 파일은 크기는 30KB 이하로 잡는 것이 좋다.
- 사이트가 크면 전체에 사용되는 스타일 시트 파일을 두고 각 페이지에 사용되는 스타일 시트를 별개로 두는 방법이 있다.
자바스크립트는 페이지의 하단에서 로딩한다.
자바스크립트는 명시적으로 비동기라고 선언하지 않으면 DOM 생성을 방해한다.
HTML 파서가 script 태그를 발견하면 스크립트의 작업이 렌더링 트리를 변경할 수 있다고 판단하기 때문이다.
## 모바일 퍼스트
모바일을 중심으로 생각하면 디자인할 때 많은 도움이 된다.
- 이 페이지의 가장 중요한 목적은 무엇인가 같은 핵심적인 질문을 고민하게 된다.
- 사용자에게 가장 중요한 기능과 콘텐츠를 구분한다.
- 재사용할 수 있는 디자인 패턴을 만들고 그것을 화면 크기에 따라 변경하는 방법을 생각한다.
- 사이트 접근성, 인터넷이 느리거나 성능이 낮은 기기에서도 접속이 용이한지 생각하게 된다.
-->