성능최적화
전반적인 프론트단 성능최적화 기본 개념
프론트단 성능최적화는 중요합니다. 실컷 잘 만들어 놨는데 느리거나 버벅이거나 로딩 스피너와 같이 기다리라는 메시지도 없으면 사용자들이 이탈해버리고 쓰는데 짜증납니다. 보통 5초 넘어버리면 껐다가 다시켜보고 10초 넘어버리면 대부분 다 이탈합니다. 백엔드, 프론트, DB 세 방면으로 다 최적화를 하는게 베스트 입니다. 통신으로 리소스 자원을 가져오는 작업이나 복잡한 알고리즘을 처리하는 과정에서 속도가 느려지는 것은 뒷단에서 최적화를 해야 합니다. 그리고 쿼리 성능이 느린건 DB쪽에서 처리해야 합니다. 랜더링이나 라우팅 할 때 버벅이는 것 등등은 프론트 성능최적화로 해결이 가능합니다.
프론트는 라이브러리가 많기 때문에 해당 라이브러리에 맞는 최적화 방법이 따로 있을텐데요. 그 전에 원론적인 순수 프론트 최적화 개념을 숙지하면 더욱더 이해하기 편하실거에요.
1. 성능최적화가 중요한 이유
1.1 사용자 경험 개선
로딩이 느리거나 지연되는 애플리케이션은 사용자 경험을 저하시켜 비즈니스에 부정적인 영향을 미칠 수 있습니다. 사용자는 빠르고 반응성 있는 상호작용을 기대하며, 성능 최적화는 이를 제공하는 데 도움이 됩니다.
1.2 검색엔진 최적화
프론트엔드 성능 최적화는 검색엔진 최적화(SEO)에도 직접적이고 간접적인 영향을 미칩니다. 특히, 검색엔진이 사용자 경험과 성능을 점점 더 중요하게 평가하는 경향이 있기 때문에, 프론트엔드 최적화는 SEO에 유리하게 작용합니다.
1.3 전환률 증가
사용자가 웹사이트나 애플리케이션에서 특정 목표(예: 구매, 가입, 클릭 등)를 완료하는 비율을 높이는 것을 의미합니다. 프론트엔드는 사용자와 직접 상호작용하는 부분이기 때문에, 잘 설계된 인터페이스와 최적화된 성능이 전환율에 큰 영향을 미칩니다.
1.4 비용 절감
이 좋은 애플리케이션은 동일한 작업 부하를 처리하는 데 더 적은 리소스(예: 서버 및 메모리)가 필요합니다. 즉, 호스팅 비용이 낮아지고 인프라 요구 사항이 줄어듭니다.
1.5 경쟁사 대비 차별화
빠르고 효율적인 애플리케이션은 애플리케이션이 느리거나 최적화가 덜 된 경쟁사와 차별화됩니다. Portent 의 조사에 따르면 1초 이내에 로드되는 웹사이트는 로드하는 데 10초가 걸리는 사이트보다 전환율이 5배 높습니다. 따라서 React 애플리케이션이 잘 작동하는지 확인하는 것은 사용자를 유지하고 경쟁 우위를 유지하는 데 중요합니다.
2.성능최적화
브라우저는 웹 페이지에 필요한 리소스를 내려받고 해석한 다음 여러 계산 과정을 거쳐 콘텐츠를 화면에 보여준다. 이를 브라우저의 랜더링이라고 하며 다운로드, 파싱, 스타일, 레이아웃, 페인트, 합성을 시행합니다.
2.1 브라우저 랜더링 과정
- 파싱
브라우저에서 웹 페이지를 로드하면 가장 먼저 HTML 파일을 다운로드한다. 파싱은 다운로드한 HTML을 해석하여 DOM 트리를 구성하는 단계이다. 파싱 중 <script />, <link />, <img /> 를 발견하면 각 리소스를 요청하고 다운로드한다. HTML 또는 리소스에 CSS가 포함된 경우에는 CSSOM 트리 구성 작업도 함께 진행합니다. DOM 은 document object model 이고 css 는 cascading style sheets object model 입니다. 각각 나무와 같은 형태로 이루어진 객체 모델이라고 생각하시면 머릿속에 생각하시기 편할거에요.
파싱과정에서 생성된 DOM, CSSOM 트리가 결합해서 스타일을 매칭시킵니다. 이러한 과정을 렌더트리가 만들어진다고 합니다.
레이아웃 단계에서는 노드의 정확한 위치와 크기를 계산한다. 노드의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산하고, 레이아웃 결과로 각 노드의 정확한 위치와 크기를 픽셀값으로 렌더트리에 반영한다. CSS에서 크기 값을 %로 지정하였다면, 레이아웃 단계를 거친 후 % 값은 계산되고 측정 가능한 픽셀 단위로 변환됩니다.
2.2 성능 개선 지표
성능 개선 지표는 두가지 관점에서 확인할 수 있습니다. 브라우저 기준과 사용자 기준입니다. 브라우저 기준에서 먼저 확인해봅시다.
- 브라우저 기준
크롬 개발자도구에서 Network 탭의 하단에 웹페이지가 로딩될 때 DOMContentLoaded, load 이벤트가 발생합니다. 해당 발생시점이 빠를수록, 그리고 두 이벤트사이의 간격이 좁을수록 성능이 좋다고 표현합다.
DOMContentLoaded : HTML, CSS 파싱이 끝나는 지점이면서 랜더 트리를 구성할 준비가 된 상항
load : HTML상에 필요한모든 리소스가 로드된 시점
그런데 최근에 많이 사용되는 프레임워크 React, Vue 에서는 HTML 양이 적어서 DOMContendLoaded, load 이벤트가 일찍 발생할 수 있습니다. 그러나 이벤트가 발생한 이후에도 많은 스크립트 실행으로 느린 로딩이 존재합니다. 그래서 해당 부분은 사용자 기준의 새로운 측정 방식에서 확인 가능합니다.
- 사용자 기준 구글 에서는 웹 페이지 로딩이 빠르다, 느리다를 느끼는 순간을 정의하고 성능지표로 사용합니다.
- FP(First Paint) 흰 화면에서 화면에 무언가가 처음 그려지는 순간
- FCP(First Contentful Paint) 텍스트나 이미지가 출력되기 시작하는 순간
- FMP (First Meaningful Paint) 사용자에게 의미있는 콘텐츠가 그려지기 시작하는 순간
- TTI (Time to Interactive) 자바스크립트의 초기 실행이 완료되어서 인터랙션이 가능 한 순간
이중에서 FMP가 가장 중요한 시점입니다. 그래서 성능을 좋게 하려면 FMP를 앞당겨야 합니다. FMP 를 앞당기면 렌더링 경로를 최적화 하여 사용자에게 긍정적인 인상을 줄 수 있습니다.
2.3 성능 측정 도구
크롬 개발자 도구에서 성능 관련 패널은 Network, Performance, Audits 가 있습니다. 각 패널의 역할과 사용방법 설명하겠습니다.
- Performance 패널 크롬개발자 도구 performance 패널의 위에서 밑으로 순차적으로 내려오는 순서입니다.
Controls
레코딩을 시작, 중단하는 영역 -> 왼쪽 상단의 아이콘
Capture
screenshots : 시간의 흐름에 따른 상태 확인
Memory : Heap Memory 상태 확인 가능
GC 버튼 : 강제로 GC 수행 가능
OverView : 전체적인 흐름 표출 영역
Main : OverView에서 선택한 구간의 상세 내용 표출
Details : Main에서 선택한 특정 항목의 상세내용 표출
- Network 패널
해당 패널에서는 웹 페이지가 로딩되는 동안 요청된 리소스의 상태를 차트 형태로 확인할 수 있으며, 리소스 최적화 상태를 비교할 때 사용합니다. 리소스 목록은 시간순으로 오름차순 됩니다. 그리고 특정 리소스를 선택하면 서버 요청 대기 시간을 자세히 볼 수 있습니다.
- Controls : 네트워크 패널의 모양과 기능을 제어
- Filters : 보여줄 리소스를 선택하는 영역
- Overview : 전체적인 요청과 다운로드 흐름 표출
- Request Table : 검색된 모든 리소스의 요청과 다운로드 상황을 표출
- Summary : 총 요청 수, 전송된 데이터 양, 이벤트 로드 시간을 표출
overview 에서 특정 영역을 선택하면 해당 리소스의 서버 요청 대기 시간을 볼 수 있습니다. 기술하겠습니다.
리소스의 서버 요청 대기 시간 보기
- Queuing : 대기열에 쌓아두는 시간
자바스크립트, CSS보다 우선순위가 낮아서 대기한다.
TCP 소켓 대기
TCP 연결 초과 대기
디스크 캐시 항목 작성 소요 시간
Stalled : 요청을 보내기 전의 대기 시간
- Queuing에서 쌓인 대기 시간 소모
- 프락시 협상에 드는 시간
DNS Lookup : DNS 조회에 소비된 시간
Initial connection : TCP 핸드셰이크/재시도 및 SSL을 포함한 연결을 설정하는 데 걸린 시간
Waiting(TTFB) : 초기 응답(Time To First Byte)을 기다리는 데 소비한 시간 (서버 왕복 시간)
Content Download : 리소스 실제 다운로드 시간
위 영역에서 TTFB 시간이 가징 길게 걸립니다. 해당 시간이 실제 백엔드에서 로직 처리하고 응답까지 받는데 걸리는 시간이기 때문이에요. 해당 구간이 너무 오래 걸린다면 백엔드 로직 또는 쿼리에 뭔가 과부하가 있거나 CPU 연산이 오래걸린다는 겁니다. 이러한 지표를 통해서 어떤 구간의 소스코드를 수정해야 할 지 예상 할 수 있습니다.
- lighthouse 패널
사용자 기준의 성능 측정 지표를 확인할 수 있다.
3. 웹페이지 로딩 최적화
3.1 javscript, css 최적화
CSS 최적화
- css 최적화 렌더링 하기위해 필요한 렌더트리를 만들기 위해서는 CSSOM 트리가 필수이다. DOM 트리는 파싱 중에 태그를 마주칠 때 마다 순차적으로 구성할 수 있다. 하지만 CSS 모두 해석해야 CSSOM 트리가 구성된다. 그래서 CSSOM 트리는 렌더링을 차단 할 수 있는 리소스다. 그러므로 차단되지 않도록 CSS 는 항상 HTML 문서 최상단에 배치해야 한다.
<head>
<link href="style.css" rel="stylesheet" />
</head>
- 미디어 쿼리를 사용해서 블로킹을 방지할 수 있다.
<link href="print.css" rel="stylesheet" media="print" />
외부 스타일시트를 가져올 때 @import 사용은 피한다. 왜냐하면 병렬로 다운로드 할 수 없기 때문이다.
종종 내부 스타일시트를 사용해서 최적화 한다.
<head>
<style type="text/css">
.wrapper {
background-color: red;
}
</style>
</head>
Javascript 최적화
자바스크립트는 DOM 트리와 CSSOM 트리를 동적으로 변경할 수 있기 때문에 HTML 파싱을 차단하는 블록 리소스입니다. <script> 태그를 만나면 스크립트가 실행됩니다. 그리고 스크립트 실행이 완료될 대까지 DOM 트리 생성이 중단됩니다. 외부에서 가져오는 스크립트 마찬가지로 리소스를 다운로드하고 실행할 때 까지 DOM 트리 생성이 중단됩니다. 그래서 자바스크립트도 렌더링 차단 리소스라고 하며, HTML 문서 최하단에 배치합니다.
<body>
<div>...</div>
<div>...</div>
<script src="app.js" type="text/javascript"></script>
</body>
만약 <script> 태그에 defer나 async 속성을 명시하면 스크립트가 DOM 트리와 CSSOM 트리를 변경하지 않겠다는 의미이기 때문에 브라우저가 파싱을 멈추지 않습니다.
3.2 리소스 최적화
리소스 요청 수 줄이기 CSS, 자바스크립트, 이미지 등의 웹 리소스는 서버 요청 후 다운로드되어야 사용할 수 있습니다. 그래서 오청과 응답시간을 줄여야 최적화가 가능합니다. 그럴려면 필요한 요청만 해야 합니다.
이미지 스프라이트 기법 : 아이콘마다 다른 이미지 파일을 사용할 경우 리소스 요청이 7번 이상 발생합니다. 이런 경우 이미지 스프라이트 기법을 이용하여 7번의 리소스 요청을 1번으로 줄일 수 있습니다.
CSS, 자바스크립트 번들하기 : webPack과 같은 번들러 사용한다. 기본적으로 vite 나 next 는 번들러가 초기세팅할 때 내장되어 있다는 점 참조바랍니다.
내부 스타일시트 사용하기 : <link> 태그로 외부 스타일시트를 가져오는 대신, 문서 안에서 <style> 태그를 사용할 수 있다. 이러한 사용 방법을 내부 스타일시트라고 하며, 외부 스타일시트를 가져올 때 발생하는 요청 횟수를 줄일 수 있다. 단, 내부 스타일시트를 사용하면 리소스 캐시를 사용할 수 없어서 HTML에 CSS가 매번 포함되므로 필요한 경우에만 사용한다.
작은 이미지를 HTML, CSS로 대체 : 웹 페이지에서 사용하는 아이콘 이미지 개수가 적은 경우, 다운로드한 이미지를 사용하는 대신 이미지를 HTML, CSS에 포함해 사용할 수 있다. Data URI로 처리할 수 있으며, 다음과 같이 HTML, CSS에서 외부 경로로 이미지를 가져오던 부분을 Base64로 변환된 URI로 대체한다. 이렇게 하면 외부 이미지를 사용하기 위해 발생하는 요청 횟수를 줄일 수 있다. 이 경우도 내부 스타일시트를 사용했을 때와 같이 캐시 문제가 있으므로 필요한 경우에만 사용한다.
중복코드 제거하기 : 자바스크립트 코드 중 자주 사용되는 코드는 utils.js 파일로 정리해 사용한다. 중복 코드로 인해 용량이 늘어나는 문제를 막을 수 있다.
만능 유틸 사용 주의하기 : lodash와 같은 만능 유틸 라이브러리를 사용할 때 주의한다. 일반적인 방식으로 가져와 사용하면 유틸 함수 전체가 포함되므로 자바스크립트 파일 용량이 커진다. 이 경우에 필요한 함수만 부분적으로 가져올 수 있으며 용량이 늘어나는 문제를 해결해준다. 그리고 되도록 사용하지 않는 기능이 많이 포함된 라이브러리 사용은 지양한다.
HTML 마크업 최적화 : 공백, 주석 등을 제거하여 사용 권장, 불필요한 마크업 사용으로 인해 DOM 트리가 커지는 것을 막는다. 파일 용량이 늘어나지 않도록 해야 합니다. 불필요한 마크업 최적화
3.3 랜더링 최적화
레이아웃 최적화
랜더링은 크게 5가지 과정을 거치는데요. javasript -> style -> layout -> paint -> composite 입니다. 이러한 과정에서 화면이 어디에 배치될지 계산하는 과정이 레이아웃 입니다. 레이아웃 최적화는 시간을 단축하고 적게 리페인트 할 수 있도록 하는 것입니다.
자바스크립트 실행 최적화
자바스크립트 자체의 실행시간이 긴 경우 렌더링 성능이 떨어지는 것은 어쩔 수 없습니다. 그러나 자바스크립트 코드가 단순한 경우인데도 불필요한 레이아웃으로 인해 실행시간이 오래 걸리는 경우는 최적화가 필요합니다.
- 강제 동기 레이아웃 최적화
JavaScript 코드에서 DOM 속성이나 메서드를 호출하면 브라우저는 현재의 레이아웃 상태를 즉시 계산해야 할 때 강제 동기 레이아웃이 발생합니다. 문제점은 성능 저하, 사용자 경험 저하, 프레임 드랍(애니메이션이나 화면 업데이트 시 프레임이 끊기는 현상)이 발생합니다. 레이아웃 영향을 미치는 요소
- DOM 업데이트 후 즉시 레이아웃 정보 요청
element.style.width = '200px' // DOM 업데이트
console.log(element.offsetWidth) // 즉시 레이아웃 계산 요청
- 자주 발생하는 DOM 읽기/쓰기 혼합 작업
element.style.height = '100px' // 쓰기
const width = element.offsetWidth // 읽기 (강제 레이아웃 발생)
element.style.width = width + 'px' // 다시 쓰기
- 해결 방법
// 잘못된 방식
element.style.width = '200px'
console.log(element.offsetWidth) // 강제 레이아웃 발생
// 올바른 방식
const width = element.offsetWidth // 먼저 읽기
element.style.width = '200px' // 나중에 쓰기
requestAnimationFrame(() => {
element.style.width = '200px'
})
- 레이아웃 스레싱 피하기
한 프레임 내에서 강제 동기 레이아웃이 연속적으로 발생하면 성능이 더욱 저하된다. 다음 코드에서는 paragraphs[i] 요소를 순회하면서 각 요소의 너비를 box 요소의 너비와 일치하도록 설정한다. 반복문 안에서 style.width를 설정하고 box.offsetWidth를 읽어오면 for문이 반복 실행될 때마다 레이아웃이 발생한다. 이것을 레이아웃 스래싱이라고 한다. 반복문 밖에서 box 엘리먼트의 너비를 읽어오면 레이아웃 스래싱을 막을 수 있다.
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
for (let i = 0; i < paragraphs.length; i += 1) {
paragraphs[i].style.width = width + 'px'
}
}
- 가능한 한 하위 노드의 DOM을 조작하고 스타일을 변경
DOM을 변경하면 스타일 계산, 레이아웃, 페인트 과정이 모두 필요하며, 조작이나 스타일 변경을 하는 DOM이 상위에 있을수록 한 프레임에 드는 시간이 길어집니다.
- 체크 항목
DOM 트리 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 미친다. 변경 범위를 최소화할수록 레이아웃 범위가 줄어든다.
- 영향받는 엘리먼트 제한 DOM과 스타일을 변경하면 레이아웃 과정에서 주변의 엘리먼트도 영향을 받아 다시 레이아웃을 해야 하는 경우가 있다.
체크 항목
부모-자식 관계 : 부모 엘리먼트의 높이가 가변적인 상태에서 자식 엘리먼트의 높이를 변경할 경우, 부모 엘리먼트부터 레이아웃이 다시 일어난다. 이때 부모 엘리먼트의 높이를 고정하여 사용하면 하단에 있는 엘리먼트는 영향을 받지 않게 된다. 예를 들어 높이가 모두 다른 여러 개의 탭 콘텐츠가 있을 때, 부모 엘리먼트(탭 컨테이너)의 높이를 고정하여 사용한다.
같은 위치에 있는 엘리먼트 : 여러 개의 엘리먼트가 인라인(inline)으로 놓여 있을 때 첫 번째 엘리먼트의 width 값 변경으로 인해 나머지 엘리먼트의 위치 변경이 일어나므로 유의한다.
- 숨겨진 엘리먼트 수정
엘리먼트가 display: none 스타일을 가지고 있으면 DOM 조작과 스타일을 변경하더라도 레이아웃과 리페인트가 발생하지 않는다. 많은 수의 엘리먼트를 변경해야 할 경우 숨겨진 상태에서 엘리먼트를 변경하고 다시 보이도록 하여 레이아웃 발생을 최대한 줄인다. visibility: hidden은 보이지 않아 리페인트는 발생하지 않지만, 공간을 차지하기 때문에 레이아웃은 발생하게 된다.
체크 항목
- display: none으로 숨겨진 엘리먼트를 변경할 경우에는 레이아웃, 리페인트가 발생하지 않아 성능에 유리하다.
HTML, CSS 최적화
화면을 렌더링하기 위해서 필요한 데이터는 HTML과 CSS로, 각각 DOM트리와 CSSOM 트리를 만들고 렌더링할 때 사용된다. DOM트리와 CSSOM 트리를 변경하면 렌더링을 유발하고 트리가 클수록 더 많은 계산이 필요하다. 그러므로 HTML과 CSS를 최적화하여 렌더링 성능을 향상할수 있다.
- CSS 규칙수 최소화 엘리먼트의 클래스를 변경하면 렌더링이 발생하는데, CSS가 복잡하고 많을수록 스타일 계산과 레이아웃이 오래 걸린다.
체크 항목
- 사용하는 규칙이 적을수록 계산이 빠르므로 최소화한다.
- 복잡한 선택자는 스타일 계산에 많은 시간이 걸리므로 사용을 피한다.
- DOM 깊이 최소화 DOM 트리가 깊을수록, 하나의 노드에 자식 노드가 많을수록 DOM 트리는 커진다. 그만큼 DOM을 변경했을 때 업데이트에 필요한 계산은 많아진다.
체크 항목
- DOM이 작고 깊이가 얕을수록 계산이 빠르다.
- 불필요한 래퍼 엘리먼트는 제거한다.
애니메이션 최적화
한 프레임 처리가 16ms(60fps) 내로 완료되어야 렌더링 시 끊기는 현상 없이 자연스러운 렌더링을 만들어낼 수 있다. 자바스크립트 실행 시간은 10ms 이내에 수행되어야 레이아웃, 페인트 등의 과정을 포함했을 때 16ms 이내에 프레임이 완료될 수 있다. 애니메이션을 구현할 때 네이티브 자바스크립트 API를 사용하는 것보다 CSS 사용을 권장한다.
- requestAnimationFrame() 사용
requestAnimationFrame API를 사용하면 브라우저의 프레임 속도(보통 60fps)에 맞추어 애니메이션을 실행할 수 있도록 해준다. 특히 setInterval, setTimeout과 달리 프레임을 시작할 때 호출되기 때문에 일정한 간격으로 애니메이션을 수행할 수 있는 장점이 있다. 또한 현재 페이지가 보이지 않을 때는 콜백함수가 호출되지 않기 때문에 불필요한 동작을 하지 않는다.
function animate() {
// 애니메이션 처리 프레임 코드
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
- CSS 애니메이션 사용
자바스크립트를 사용한 애니메이션은 성능이 나쁠 수 있다. CSS3 애니메이션을 사용하면 자바스크립트를 실행할 필요가 없고, 브라우저가 애니메이션을 처리하는데 최적화되어 있어서 부드러운 애니메이션을 구현할 수 있다. CSS 애니메이션을 구현할 때는 다음 사항을 지켜서 사용한다.
- position: absolute 처리
애니메이션 영역이 주변 영역에 영향을 주지 않도록 주의해야 한다. position을 absolute나 fixed로 설정하면 주변 레이아웃에 영향을 주지 않는다.
- transform 사용
스타일 속성 중 position, width, height 등과 같이 기하적 변화를 유발하는 속성을 변경하면 레이아웃이 발생한다. transform을 사용한 엘리먼트는 레이어로 분리되기 때문에 영향받는 엘리먼트가 제한되어 레이아웃과 페인트를 줄일 수 있다. 그리고 합성만 발생시키기 때문에 애니메이션에서 사용 시 렌더링 속도가 향상할 수 있다. 때에 따라 하드웨어가 지원될 경우 GPU를 사용할 수 있으므로 성능이 빠르다. 예를 들어 left, top을 사용하면 모든 프레임마다 엘리먼트와 배경이 합성되어 많은 시간이 걸리므로, transform: translate()를 사용해야 한다.
body {
background-color: lime;
}
.animation-item {
position: absolute; /* good */
top: 0;
left: 0;
width: 50px;
height: 50px;
background-color: red;
animation: move 3s ease infinite;
}
/* bad */
@keyframes move {
50% {
top: 100px;
left: 100px;
}
}
/* good */
@keyframes move {
50% {
transform: translate(100px, 100px);
}
}