# 웹 성능 최적화 ###### 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이 해당된다. ![](https://i.imgur.com/tW5oDXI.png) ### Repaint - 높이, 넓이, 위치 등의 기하하적 영향이 없는 경우 Layout 과정 없이 Paint가 일어난다. - 즉, 크기와 위치를 계산하는 시간이 없어졌기 때문에 Reflow가 아닌 대안으로 사용하길 지향한다. - background-color, color, visibility, text-decoration이 이에 해당된다. ![](https://i.imgur.com/jqcy121.png) ## 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 플러그인 같은 도구를 사용해 압축 ## 성능 측정 지표 ### 브라우저 기준의 측정 ![](https://i.imgur.com/vYTGeUm.png) - `DOMContentLoaded`나 `load` 발생 시점이 빠를 수록 성능이 좋다. - `DOMContentLoaded`나 `load` 발생 구간 폭이 좁을수록 성능이 좋다. - DOMContentLoaded는 HTML과 CSS 파싱이 끝나는 시점에 발생하는 이벤트, Render Tree를 구성할 준비가 된 상황 - load는 HTML에 필요한 모든 리소스가 준비된 시점의 이벤트 - 하지만 SPA같은 패러다임으로, 적은 양의 HTML을 가져와 위의 이벤트들이 빠르게 동작할 수 있지만 사용자에게 느린 성능의 페이지를 제공할 수도 있다. ### 사용자 기준의 측정 - 완료 이벤트와 상관없이 웹 페이지의 렌더링 성능을 측정한다. #### 주요 렌더링 경로 ![](https://i.imgur.com/BIVcyVT.png) ![](https://i.imgur.com/qSHY7Z5.png) 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 패널 ![](https://i.imgur.com/qi7nXtT.png) - 웹 페이지 로드 과정을 레코딩하고 그 단계마다 시간을 확인할 수 있다. ### 2. Network 패널 ![](https://i.imgur.com/OtJUpyL.png) - 웹 페이지 로드 과정에서 서버에게 요청된 리소스의 상태를 확인할 수 있다. #### 서버 요청 대기 시간 ![](https://i.imgur.com/eK4SHgP.png) 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 패널 ![](https://i.imgur.com/8IfCwYS.png) 사용자 기준의 성능 측정 지표를 확인할 수 있다. 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 태그를 발견하면 스크립트의 작업이 렌더링 트리를 변경할 수 있다고 판단하기 때문이다. ## 모바일 퍼스트 모바일을 중심으로 생각하면 디자인할 때 많은 도움이 된다. - 이 페이지의 가장 중요한 목적은 무엇인가 같은 핵심적인 질문을 고민하게 된다. - 사용자에게 가장 중요한 기능과 콘텐츠를 구분한다. - 재사용할 수 있는 디자인 패턴을 만들고 그것을 화면 크기에 따라 변경하는 방법을 생각한다. - 사이트 접근성, 인터넷이 느리거나 성능이 낮은 기기에서도 접속이 용이한지 생각하게 된다. -->