프론트엔드 웹 성능 최적화
약 2달 동안 몇 벌의 프로모션 페이지를 제작하면서 한국이 아닌 다른 국가에서의 웹 속도가 아주 느리다는 것을 깨달았다. WebpageTest에서 싱가폴, 일본, 미국, 베트남 환경에서의 프로모션 페이지 성능을 측정해 봤고 결과는 처참했다.. 🥲 이렇게 답답하다니!
이를 조금이나마 개선하기 위해 백엔드 개발자분과 여러 방법을 알아보고 적용하게 되었다. 결론적으로는 4개 국가 프로모션 페이지 LCP avg 0.4s / FCP avg 0.2s
감소라는 최적화 결과를 얻게 되었다. (너무 미미한 차이긴 하지만..)
이 개선 작업을 계기로 웹 렌더링 최적화 글을 정리하게 되었다.
웹 성능 최적화란?
웹 성능 최적화란 단순하게 웹을 랜딩하는데에 필요한 각종 비용을 줄이는 것이라고 할 수 있다.
웹 프론트 환경에서는 각종 리소스들을 주고 받으며 화면을 띄우고, 화면을 업데이트하는 과정에서 지속적으로 비용을 소모한다.
따라서 이러한 웹 프론트엔드 성능 개선을 위해서 웹 개발자는 최소한의 데이터로 가장 빠른 시간에 사용자가 불편함을 느끼지 않는 최적의 화면을 띄워야 한다.
* 브라우저 랜더링 과정
- 브라우저의 호스트 파일, 캐시에 URL 파일 정보가 있는지 확인
- 이전에 접속한 적이 있어 캐시가 존재하는 경우, DNS 서버를 통하지 않아도 된다.
- DNS 서버를 통해 실제 서버의 IP 주소를 받아와 리소스 요청 및 응답을 받아온다.
- 서버의 응답으로 받아온 HTML, CSS 파일을 파싱해 각각 DOM 트리, CSSOM 트리를 만든다.
- 각 트리를 스타일 정보에 맞게 Attachment 작업을 거쳐 랜더 트리를 만든다.
- 랜더 트리를 화면의 어느 위치에, 어떤 크기로 배치할 것인지 계산하는 작업인 레이아웃 과정을 거친다.
- 실제 화면에 그리는 페인트 과정을 거친다.
만약 브라우저 랜더링 과정에서 사용자 인터렉션으로 인한 DOM 수정, 또는 CSS 규칙 수정으로 화면이 재랜더링 되어야 할 경우, 위의 단계들이 다시 실행된다.
따라서 위의 브라우저 랜더링 프로세스를 최적화하는 것이 곧 성능 최적화하는 방법이라고 할 수 있다.
성능 최적화
성능최적화는
- 페이지를 로드할 때
- 페이지를 랜더링할 때
두 시점으로 분류가 가능하며, 각각의 경우에 적용할 수 있는 성능 최적화를 알아본다.
01. 페이지 로드 최적화
블록 차단 리소스 최적화
🚩 블록 차단 리소스란?
HTML을 파싱할 때, 해당 리소스를 만나면 잠시 HTML 파싱 작업을 중단하고 권한을 넘겨받게 되는 리소스들. (CSS 파일, JS 파일)
블록 차단 리소스는 DOM 생성 프로세스 (파싱)을 중지해 초기 랜더링이 지연되기 때문에 랜더링 차단 요소이기도 하다
따라서 올바른 실행 위치에서 코드를 작성해야 한다
CSS, JS 코드 위치
- CSS: head 태그 안
- JS: body의 맨 하단
특정 속성 사용
- CSS:
media
attribute로 어떤 단말기인지에 따라 해당 CSS를 적용할 것인지 명시rel=”preload” as=”style”
로 load 이벤트를 막지 않으면서 css 파일 요청 가능onload=”this.onload=null;this.rel=’stylesheet’”
로 css 파일이 로드 이벤트 이후에 파싱되고, onload 함수 제거가 보장된다
- JS
defer
: 비동기적으로 파일 로드, 모든 DOM이 로드된 이후 스크립트 실행 (실행 순서 보장)async
: 비동기적으로 파일 로드하지만, 스크립트 로드만 병렬적으로 실행 (실행 순서 미보장)
리소스 용량 줄이기
가장 직관적인 방법으로, 리소스의 용량을 줄임으로써 다운로드 시간을 최적화할 수 있다.
🚩 리소스?
- XHR : xml http request로, 요청과 요청에 따른 응답 결과 확인용으로 리소스에서 제외
JS 용량 최적화
- 트리 쉐이킹
- 외부 모듈에서 필요한 기능만을 import
(
import _ from ‘lodash’
보다는,import array from ‘lodash’
)
- 외부 모듈에서 필요한 기능만을 import
(
- 불필요한 코드 제거!
- tab size는 2 spaces
CSS 용량 최적화
- 간결한 selector
- 공통 스타일은 class로 정의해서 묶어 사용한다,
이미지, 미디어, 폰트 용량 최적화
-
이미지의 경우, png보다 jpg, jpeg의 이미지 크기가 더 작으므로 해당 확장자 사용
- 애니메이션 요소의 경우, gif보다 video태그로 mp4 파일 사용
- 글꼴의 경우, 서비스에서 지원하고자 하는 브라우저와 언어에 따라 파일을 적절하게 가려 import
리소스 요청 갯수 줄이기
리소스 요청 갯수를 줄이는 것으로 최적화할 수 있다.
이미지 요청 갯수 최적화
- 이미지 스프라이트
- 각각의 이미지 파일을 서버로 요청하는 것 보다,
이미지를 하나로 묶어 한번의 요청을 통해 가져와
background-position
속성으로 원하는 부분만 표시하는 기법을 말한다.
- 각각의 이미지 파일을 서버로 요청하는 것 보다,
이미지를 하나로 묶어 한번의 요청을 통해 가져와
- 이미지 lazy loading
- 사용자 화면에 당장 보이는 이미지만 요청하고, 사용자가 스크롤을 내려 다른 이미지가 보여야 할 때 이미지를 요청하는 기법이다. MUSINSA
CSS, JS 파일 요청 갯수 최적화
-
모듈 번들러로 CSS, JS 번들링
webpack
과 같은 모듈 번들러로 여러개의 js 파일을 하나의 파일로 번들링- 참고로
webpack
은 공통 기능 단위로 JS 파일을 자체적으로 코드 스플리팅하기도 한다.
-
캐싱할 필요가 없는 style은 내부 스타일시트 이용
link
tag로 가져오는 외부 스타일 시트가 아닌,<style>
태그로 포함하는 내부 스타일 시트를 사용해 외부 css 요청 횟수를 줄인다.- 다만 내부 스타일 시트는 캐싱되지 않는다.
-
모든 리소스 캐싱
- 이미지, 폰트 등의 정적 리소스 또는 CSS, JS등의 파일들을 전부 클라이언트나 서버 캐시에 저장해둔다.
- 해당 컨텐츠를 재호출할 때 서버 요청을 통하지 않고 캐시에서 가져와 활용할 수 있다.
02. 페이지 랜더링 최적화
랜더링 최적화의 목표는, 비용이 큰 레이아웃 작업을 최대한 빠르게, 최대한 적게 발생시키는 것이다.
JS 랜더링 최적화
-
강제 동기식 레이아웃, 레이아웃 스래싱 피하기
- 레이아웃 과정이 끝나기 전에 JS 파일에서 해당 DOM 요소의 위치나 크기 값을 변경 후 바로 가져오려 하면 강제로 레이아웃을 발생시킨다, → 강제 동기식 레이아웃
- 이러한 강제 동기식 레이아웃이 반복문 내에서 연속적으로 사용 → 레이아웃 스래싱
function logBoxHeight() { box.classList.add("super-big"); // Gets the height of the box in pixels // and logs it out. console.log(box.offsetHeight); }
불필요한 레이아웃을 추가적으로 발생시키기 때문에 지양해야 한다.
-
상위 DOM 요소보다 하위 DOM요소 사용
- 상위 DOM 요소를 사용하면 하위 DOM에도 영향을 미치기 쉬우므로, 가급적 하위 DOM 사용
-
display : none
은 레이아웃이 발생하지 않는다.display : none
의 스타일을 가진 DOM은 렌더 트리에 포함되지 않는다. (visibility: none
은 포함되므로 성능 최적화에 도움이 되지 않는다.)
-
domFragment
활용documentFragment
는 실제 DOM 트리에 포함되는 요소가 아니므로, 리플로우나 리페인트를 발생시키지 않는다.
const parentNode = document.getElementById("parent"); const cnt = 10; const fragNode = document.createDocumentFragment(); for (let i = 0; i < cnt; i++) { const newNode = document.createElement("li"); newNode.innerText = `this is ${i}-content`; fragNode.appendChild(newNode); } parentNode.appendChild(fragNode);
위 작업처럼
domFragment
에 추가된 요소들을parentNode
에 append함으로써 여러개의 추가 작업을, 한 번만 DOM 객체에 접근함으로써 구현할 수 있다. -
requestAnimationFrame API
활용setTimeout
,setInterval
을 사용하면 실행 시간이 보장되지 않아 사용자에게 끊김없는 애니메이션을 제공하기 어렵다.requestAnimationFrame
은 자바스크립트의 프레임 시작과 동시에 애니메이션이 프레임의 시작과 함께 실행되는 것을 보장해주며, 화면에 해당 애니메이션 요소가 보이지 않을 시, 콜백 함수를 호출하지 않는다. (최적화 가능)
CSS, HTML 렌더링 최적화
-
CSS에 복잡한 selector 규칙 사용 지양
- CSS가 복잡하고 많을수록 스타일 계산과 레이아웃 과정이 오래 걸린다.
-
DOM 트리와 Style 트리 단순화
- 불필요한 wrapper element를 피하자
-
애니메이션 요소 position 고정
- 다른 요소에 영향을 미칠 수 있다. 연쇄적인 리플로우, 리페인트를 발생시킬 수 있다.
position: absolute, fixed
로 고정하는 것이 좋다.
-
리플로우보다 리페인트를 발생시키는 속성 활용
-
참고 링크 : What forces layout/ reflow
-
top, left, right, bottom, width, height를 직접 조작하는 것 보다
transform
속성을 활용하자
-
웹 페이지 성능 측정 지표
사용자에게 컨텐츠를 보여주는 시점을 기반으로 여러 가지로 나뉜다.
웹 성능은 구글 개발자 도구의 라이트하우스에서 간단하게 측정 가능하다.
- 개인적으로 사용한 측정 페이지 : WebPageTest
FCP(First Contentful Paint)
페이지가 로드를 시작한 시점부터, 의미있는 컨텐츠가 처음 렌더링 되는 시점
🚩 의미 있는 컨텐츠?
텍스트, 이미지, SVG, canvas등의 요소
→ 사용자가 현재 이 웹 페이지가 로드되고 있구나 라는 것을 인지할 수 있는 요소
LCP (Largest Contentful Paint)
사용자의 뷰포트에서 가장 큰 이미지, 혹은 텍스트 블록이 렌더링 되는 시간 측정
IMG, SVG 내부의 이미지, 비디오, 텍스트, 배경이미지 등의 가장 큰 요소가 대상이 되며,
요소의 크기는 CSS와 관련된 부분을 제외한 후,
이미지의 원본 크기와 랜더링 된 크기가 다를 경우 더 작은 값으로 기준을 삼는다.
가장 큰 요소를 기준으로 하기 때문에, 이후에 로드된 요소가 더 큰 경우 측정 대상을 바로 변경하며
사용자와 상호작용이 가능한 시점이 되면 측정을 중지한다.
Speed Index
웹 페이지가 얼마나 빨리 컨텐츠를 채우는지를 측정하는 지표이다.
A와 B의 경우 페이지가 로드되는 시점은 동일하지만
A가 더 빠르게 컨텐츠들을 채워주기 때문에 사용자는 A가 더 빠르다고 인식할 수 있다.
따라서 Speed Index 지표의 더 좋은 점수를 받을 수 있다.
그 외
이 외에도 CLS, TBT 등의 많은 지표가 존재한다.
- 기존에 뉴진스 프로모션 페이지로 진행했던 테스트 결과 (방콕 기준)
출처
coffeeandcakeandnewjeong tistory
sisofiy626 velog :웹-페이지-성능-측정-지표
댓글남기기