지난 시간에 주소창에 www.naver.com을 입력하고 사용자에게 네이버 페이지가 보이기까지의 과정에 대해 포스팅을 작성하였다.
해당 포스팅의 내용을 바탕으로 F-lab
멘토링을 진행하였고, 전체적인 내용에는 부족함이 없으며 면접 질문에 대한 답변 내용으로 충분하다는 피드백을 받았다.
다만 아쉬운 점이 있다면, DNS 캐시
등 데이터 통신 과정에서의 최적화 방법에 대한 내용에 대해서는 공부했으나 렌더링 과정에서의 최적화 방법에 대해서 내용이 약간 부족했다.
따라서 이번 포스팅에서는 렌더링 과정에 더욱 집중하여 HTTP 캐시
, 렌더링 성능 최적화
등 렌더링 과정에 대한 심화 내용을 다룰 예정이다.
기본적인 렌더링 과정은 이전 포스팅을 참고하길 바라며, 이번 포스팅에서는 심화적인 내용에 대해서만 포스팅 하도록 하겠다.
캐시
컴퓨터 과학에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 더 빠른 속도로 데이터에 접근할 수 있다.
출처 : https://ko.wikipedia.org/wiki/%EC%BA%90%EC%8B%9C
프론트엔드 개발자로서 캐시를 재정의 해보자.
"웹 페이지의 로딩 속도를 높여 사용자 경험을 향상시키고, 네트워크 대역폭을 절약하여 서버 부하를 감소시키는 기술" 정도로 정의할 수 있을 것 같다.
프론트엔드 개발자는 주로 HTTP 캐시를 사용하여 브라우저 캐시를 설정한다.
캐시는 크게 private 캐시
와 shared 캐시
두 가지로 나뉜다.
private 캐시
는 특정한 클라이언트에 저장되는 캐시를 말한다. (ex. 브라우저 캐시)
다른 클라이언트와 공유되지 않으므로 해당 사용자에 대한 개인화된 응답을 저장할 수 있다.
local 캐시
라고도 한다.
shared 캐시
는 클라이언트와 서버 사이에 존재하며, 한 명 이상의 사용자가 공유할 수 있는 응답을 저장하는 캐시를 말한다. (ex. 프록시 캐시)
프론트엔드 개발자가 주로 다루는 캐시는 브라우저 캐시로 해당 포스팅에서도 브라우저 캐시를 위주로 다룬다.
참고자료
MDN Types of caches
https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#types_of_caches
HTTP 캐시
를 사용하는 이유는 위에서 내린 정의와 일맥상통하다.
브라우저 캐시를 사용하면 정적인 파일(HTML, CSS, JS, 이미지 등)
들을 브라우저 캐시에 저장하여 사용자가 같은 리소스를 요청할 때, 다시 다운로드 받지 않고 캐시에서 불어올 수 있다.
이는 불필요한 네트워크 요청을 줄이고 웹 페이지 로딩 속도를 향상 시킨다.
같은 리소스에 대해 다시 서버에 요청하지 않고, 캐시된 리소스를 사용하므로 서버와의 네트워크 비용을 줄일 수 있다.
사용자가 많은 경우 효과적으로 서버의 과부하를 줄일 수 있다.
일반적으로 GET
요청에 대한 응답만을 캐싱하며, 다른 메서드들은 제외된다.
일반적인 캐싱 엔트리는 다음과 같다.
Http Request
에 포함된 Cache-Control 헤더
에 따라 리소스(HTML, CSS, JS 등)
의 생명 주기가 결정된다.
캐시의 유효기간을 설정하는 방법으로는 max-age
와 Expires
가 있다.
max-age=<seconds>
는 캐시의 유효 시간을 초 단위로 지정한다.
Expires: <http-date>
는 캐시가 유효한 날짜/시간을 지정한다.
max-age
와 Expires
가 둘 다 있는 경우 max-age
가 우선된다.
만약 리소스의 유효기간이 지나기 전이라면, 브라우저는 서버에 요청을 보내지 않고 디스크
또는 메모리
에서 캐시를 읽어와 사용한다.
max-age
와Expires
중 어떤 헤더를 사용해야할까?
max-age
는HTTP/1.1
에서 지원하는 헤더로HTTP/1.0
에서는 사용할 수 없다.
Expires
는 하위 호환성을 위해 사용되기도 하지만 현재는HTTP/1.1
의 대중화로 거의 사용되지 않는다.
참고자료
MDN Expires or max-age
https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#expires_or_max-age
캐시의 유효기간이 지난 경우에도 캐시는 삭제되지 않는다.
브라우저는 유효기간이 지난 리소스 요청이 발생하면, If-Modified-Since
또는 If-None-Match
요청 헤더를 포함한 HTTP conditional requests
를 통해 유효성을 검증한다.
이 과정을 validation
또는 revalidation
이라고 한다.
변경사항이 없는 경우, 서버는 304 Not Modified
라는 응답을 보낸다.
이 응답은 response body
없이 status code
만 포함하므로 매우 작다.
변경사항이 있는 경우, 200 OK
또는 적절한 상태 코드와 함께 최신 리소스를 응답한다.
If-Modified-Since
vsIf-None-Match
If-Modified-Since
Last-modified
이후 변경사항의 여부에 대해 묻는다.GET /index.html HTTP/1.1 Host: example.com Accept: text/html If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
If-Modified-Since
방식은 시간 형식이 복잡하고parsing
에 어려움이 있으며, 분산 서버에서 파일 업데이트 시간을 동기화하기 어렵다는 단점이 있다.
이러한 문제를 해결하기 위해ETag
응답 헤더 사용하는 방식을 표준으로 정했다.
If-None-Match
ETag
값의 일치 여부를 확인한다.
ETag
는 서버에서 임의로 생성한 값이다.GET /index.html HTTP/1.1 Host: example.com Accept: text/html If-None-Match: "33a64df5"
ETag
와Last-modified
가 모두 존재하는 경우ETag
를 우선 사용한다.
참고자료
MDN validation
https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#validation
no-cache
vs no-store
no-cache
no-cache
는 캐시하지만 유효성 검증을 강제한다.
Cache-Control: max-age=0, must-revalidate
가 no-cache
와 같은 의미로 사용되지만, HTTP/1.1
가 대중화된 지금은 no-cache
를 사용해야 한다.
no-store
no-store
은 캐시의 저장 자체를 막는다.
브라우저는 어떠한 경우에도 캐시 저장소에 리소스를 저장하지 않는다.
이외에도 다양한
Cache-Control
값이 있으며, 아래 링크를 통해 확인할 수 있다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
캐시의 생명주기가 길다면 오랜 기간동안 네트워크 요청 없이 캐시된 리소스를 사용할 수 있다는 장점이 있다.
하지만, 서버의 리소스가 업데이트 되었음에도 불구하고 캐시의 유효기간이 남아있다면 사용자는 업데이트되지 않은 구버전의 리소스를 보게 될 것이다.
이러한 문제를 해결하기 위해 Cache Busting(캐시 무효화)
전략을 사용한다.
Cache Busting
(캐시 무효화)Cache Busting
은 Cache
가 URL
을 사용하여 저장된다는 점을 이용한다.
다시 말해, 버전 별로 URL
을 변경하여 브라우저가 캐시된 리소스를 사용하지 않고 새로운 리소스를 요청하도록 만든다.
하지만, www.example.com/index.html
과 같이 URL
이 유지되어야 하는 경우가 있다.
또한, HTML
파일의 경우 배포시 내부의 값이 변경될 수 있다.
이처럼 항상 새로운 버전의 배포가 있는지 확인이 필요하거나 해당 리소스가 main
리소스인 경우, max-age=0
또는 no-cache
를 적용한다.
Toss 프론트엔드
에서는 HTML
파일에 max-age=0, s-maxage=31536000
를 적용하여 CDN
에는 캐시하고 브라우저
에는 재검증을 하는 방식을 선택했다.
빌드 될때마다 새롭게 생성되는 JS
, CSS
파일 등의 경우에는 최댓값인 max-age=31536000
을 설정하여 새롭게 배포가 일어나지 않는다면 캐시의 저장된 리소스를 사용하도록 설정한다.
이때, Cache Busting
을 사용하는 방법은 다음과 같다.
(ex. style.v2.css)
(ex. /v2/style.css)
(ex. style.css?ver=2)
참고자료
web.dev HTTP Cache
https://web.dev/i18n/ko/http-cache/#tips
토스 웹캐시 다루기
https://toss.tech/article/smart-web-service-cache
cache-busting
https://www.keycdn.com/support/what-is-cache-busting
아래 그림은 브라우저의 렌더링 과정(Critical Rendering Path)
에 대해 간략하게 표현한 것이다.
브라우저는 위의 그림과 같이 CRA(Critical Rendering Path)
에 의해 렌더링이 진행되며 렌더링 성능 최적화란 CRA
과정에서 발생하는 웹 페이지 로딩
및 렌더링
최적화를 말한다.
포스팅에서 다루는 방법 외에도 렌더링 성능을 최적화시키는 방법이 있을 수 있지만, 본 포스팅에서는 TOAST UI 성능최적화 아티클을 참조하여 렌더링 성능 최적화에 대해 다룬다.
참고자료
브라우저 동작 원리
https://poiemaweb.com/js-browser
성능의 개선점을 찾기 위해서는 성능 측정에 사용되는 지표를 확인해야 한다.
성능 측정의 기준은 크게 브라우저
와 사용자
기준으로 나뉜다.
해당 내용에 대해서는 TOAST UI
아티클의 링크로 대체한다.
참고자료
성능 개선 지표(브라우저 기준, 사용자 기준)
https://ui.toast.com/fe-guide/ko_PERFORMANCE#%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EC%A7%80%ED%91%9C
먼저 로딩 과정에 대한 최적화에 대해 알아보자.
로딩 최적화 방법에는 크게 블록 리소스 최적화
, 리소스 용량 줄이기
, 리로스 요청 수 줄이기
, 캐시 사용
등이 있다.
CSS
와 JavaScript
는 블록 리소스이다.
따라서 CSS
와 JavaScript
리소스를 최적화 하는 것이 중요하다.
블록 리소스
HTML
파싱이 일어날때 파싱을 블록 시키는 원인이 되는 리소스
CSS
는 반드시 HTML
문서 최상단에 배치한다.
렌더 트리를 구성하기 위해서는 DOM
과 CSSOM
이 필요하며, CSSOM
이 생성되지 않으면 렌더 트리가 생성되지 않아 렌더링이 차단된다.
따라서 CSS
코드는 head
태그 내에 위치시켜 렌더링이 차단되지 않도록 한다.
미디어 쿼리
를 적용한다.
media
속성을 적용하여 해당 스타일을 사용하는 경우에만 로드하도록 한다.
media
태그를 사용하지 않을 경우 불필요한 블로킹이 발생할 수 있으며, lighthouse
에서는 이런 태그를 렌더 블로킹 리소스로 판단한다.
<link href="style.css" rel="stylesheet" />
<link href="print.css" rel="stylesheet" media="print" />
<link href="portrait.css" rel="stylesheet" media="orientation:portrait" />
<link href="style.css" rel="stylesheet" media="(min-width:320px) and (max-width:768px)">
@import
사용을 지양한다.
@import
는 외부 CSS
리소스를 가져오는 것이다.
웹 브라우저는 @import
가 포함된 CSS
파일을 다운로드하고 해석한 다음, 가져온 CSS
파일을 다시 다운로드하고 해성해야 한다.
따라서 @import
를 지양하고 <link>
를 사용하는 것이 최적화에 유리하다.
JS
는 반드시 HTML
문서 최하단에 배치한다.
JavaScript
는 DOM
에 접근 및 수정, 삭제가 가능하기 때문에 HTML
파싱 중 <script>
태그를 만나면 DOM
생성이 중단되고 script
코드를 파싱 및 실행한다.
따라서 <style>
태그는 <body>
태그 최하단에 위치시켜 렌더링이 차단되지 않도록 한다.
async
, defer
속성을 사용한다.
async, defer
속성에 대해서는 이전 포스팅을 참고하자.
용량이 큰 리소스는 리소스의 다운로드 시간을 증가시켜 로딩 시간을 증가시킨다.
따라서 불필요한 용량을 줄이는 것이 중요하다.
중복되는 코드의 경우 utils.js
등의 파일에 따로 분리하여 함수로 만들어 사용한다.
lodash
등 유틸리티 라이브러리 사용시 member style imports
사용을 지양하고 default style imports
를 사용한다.
import { merge } from 'lodash' // member style import
import merge from 'lodash/merge' // default style import
member style import
를 사용하는 경우 모든 lodash
함수가 포함된 lodash.js
파일을 가져와 사용한다.
또한, 사용하지 않는 기능이 많은 라이브러리의 사용을 지양한다.
불필요한 공백, 주석 등을 제거하고 태그의 중첩을 최소화하여 단순하게 구성한다.
id
선택자 대신 class
선택자를 사용하는 등 중복되는 스타일을 최소화할 수 있도록 선택자를 간결하게 사용한다.
Webpack
플러그인 등을 사용하여 HTML, CSS, JavaScript
파일을 압축하여 사용한다.
불필요한 공백이나 줄바꿈을 제거하여 용량을 줄이고 난독화 시킬 수 있다.
요청해야 하는 리소스 파일이 많으면 리소스를 다운받는데 많은 통신 비용과 시간이 필요하다.
따라서, 필요한 요청만 할 수 있도록 리소스 요청 수를 줄이는 것은 로딩 시간 단축에 도움을 준다.
스프라이트 이미지란 여러 개의 이미지를 하나의 이미지 파일로 합쳐서 관리하는 기술이다.
스프라이트 이미지를 사용하면 네트워크 요청 횟수를 감소 시켜 통신 비용을 줄일 수 있다.
또한, 하나의 이미지만을 캐싱하면 되므로 캐싱 효과가 향상된다.
하지만, 스프라이트 이미지는 유지보수에 어려움이 있으며 이미지를 사용하기 위해 추가적인 작업이 필요하다는 단점이 있다.
HTTP/2.0
프로토콜을 사용하면 Multiplexed Streams
을 지원하므로 스프라이트 이미지를 사용하지 않더라도 여러 이미지를 효율적으로 다운로드 할 수 있다.
하지만, HTTP/2.0
에서도 스프라이트 이미지를 사용하면 웹 페이지 성능 향상에 도움을 줄 수 있으므로 손익을 따져 사용 여부를 결정해야 한다.
webpack
과 같은 모듈 번들러를 사용하여 CSS
, JavaScript
파일을 번들링한다.
이러한 번들러는 여러 개의 모듈 파일을 하나로 묶어 생성하며 이를 번들 파일
이라고 한다.
모듈 번들러 사용시 media query 적용
앞에서
css 최적화
를 위해미디어 쿼리
를 적용한다고 했다.
하지만, 모듈 번들러를 사용하면 여러css
파일을 하나의 파일로 번들링한다.
따라서webpack
등의 모듈 번들러를 사용할 때에는media-query-plugin
과 같은 플러그인을 사용하여미디어 쿼리
를 적용할 수 있다.
npmjs media-query-plugin
https://www.npmjs.com/package/media-query-plugin
내부 스타일 시트를 사용하면 리소스 요청을 줄일 수 있다.
다만, 이 경우 CSS
가 HTML
내부에 포함되므로 중복된 스타일에 캐싱의 이점을 활용할 수 없다.
따라서 필요한 경우에만 적절히 사용하도록 해야한다.
네트워크 요청을 줄이기 위해 이미지를 Base64
로 변환된 URI
로 대체하여 사용한다.
단, 이 경우도 동일한 이미지를 다른 곳에서 사용할 경우 캐싱의 이점을 활용할 수 없다.
HTTP 캐시
를 활용하여 리소스를 재사용한다.
본 포스팅의 HTTP 캐시
를 참고하자.
웹 페이지를 화면에 렌더링하기 위해서는 DOM
과 CSSOM
이 필요하다.
또한, 렌더링 이후 사용자와 상호작용을 통해 다양한 기능을 구현하기 위해서는 JavaScript
코드가 필요하다.
따라서 렌더링을 최적화 하기 위해서는 HTML
, CSS
뿐 아니라 JavaScript
를 최적화 해야한다.
레이아웃(Reflow)
을 최대한 적게하고 리페인트(Repaint)
만 실행하도록 JS
, HTML
, CSS
의 관점에서 알아보자.
레이아웃이 완성되기 이전, 요소의 크기나 위치 등 계산된 값을 속성으로 읽으려고 시도하면 강제 동기 레이아웃
이 발생한다.
// 강제 동기 레이아웃이 발생하는 예시
const box = document.querySelectById("box");
box.style.width = 10px;
console.log(box.offsetWidth);
box.style.height = 20px;
한 프레임 내에서 강제 동기 레이아웃
이 연속적으로 발생하는 것을 레이아웃 스레싱
이라고 하며 레이아웃 스레싱
이 발생하지 않도록 코드를 작성해야 한다.
DOM
트리의 상위 노드를 변경하면 모든 하위 노드에 영향을 미친다.
따라서 DOM
트리를 조작할때는 변경 범위를 최소화할 수 있도록 가능한 하위 노드를 조작해야 한다.
부모-자식 관계 : 부모 엘리먼트의 높이가 가변적인 상태에서 자식 엘리먼트의 높이를 변경할 경우, 부모 엘리먼트부터 레이아웃이 다시 일어난다. 이때 부모 엘리먼트의 높이를 고정하여 사용하면 하단에 있는 엘리먼트는 영향을 받지 않게 된다. 예를 들어 높이가 모두 다른 여러 개의 탭 콘텐츠가 있을 때, 부모 엘리먼트(탭 컨테이너)의 높이를 고정하여 사용한다.
같은 위치에 있는 엘리먼트 : 여러 개의 엘리먼트가 인라인(inline)으로 놓여 있을 때 첫 번째 엘리먼트의 width 값 변경으로 인해 나머지 엘리먼트의 위치 변경이 일어나므로 유의한다.
display : none
속성을 가진 엘리먼트는 DOM
조작을 하더라도 리플로우와 리페인트가 발생하지 않는다.
애니메이션을 구현할 때에는 자바스크립트 API보다 CSS
를 사용하여 구현하는 것을 권장한다.
requestAnimationFrame()
사용requestAnimationFrame
을 사용하면 브라우저의 프레임 속도에 맞추어 애니메이션을 실행할 수 있도록 해준다.
아래 예시는 requestAnimationFrame
을 사용하여 레이아웃 스레싱
을 개선한 예시이다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box');
for (let i = 0; i < 100; i++) {
box.style.left = `${i}px`;
box.style.top = `${i}px`;
box.style.width = `${100 - i}px`;
box.style.height = `${100 - i}px`;
box.style.backgroundColor = `rgb(${i}, 0, 0)`;
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px;
height: 100px;
background-color: red;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box');
const changes = [];
for (let i = 0; i < 100; i++) {
changes.push({
left: `${i}px`,
top: `${i}px`,
width: `${100 - i}px`,
height: `${100 - i}px`,
backgroundColor: `rgb(${i}, 0, 0)`,
});
}
let current = 0;
function animate() {
const change = changes[current];
box.style.left = change.left;
box.style.top = change.top;
box.style.width = change.width;
box.style.height = change.height;
box.style.backgroundColor = change.backgroundColor;
current++;
if (current < changes.length) {
requestAnimationFrame(animate);
}
}
animate();
</script>
</body>
</html>
CSS 애니메이션
을 사용하면 자바스크립트를 실행할 필요 없으며, 브라우저가 애니메이션을 처리하는데 최적화되어 있다.
따라서 CSS 애니메이션
을 사용하면 부드러운 애니메이션을 구현할 수 있다.
CSS 애니메이션
을 사용할 때는 다음 사항을 지켜야 한다.
애니메이션 영역이 주변 영역에 영향을 주지 않도록 absolute
혹은 fixed
값을 설정해야한다.
width
, height
, position
등 기하학적 변화를 유발하는 속성을 변경하면 리플로우
가 발생한다.
tranform
속성을 사용하면 레이어가 분리되어 레이어 합성만 발생하고 GPU
를 사용할 수 있으므로 성능이 빠르다.
따라서 transform : translate()
를 사용하여 애니메이션을 구현한다.
참고자료
TOAST UI 성능 최적화
https://ui.toast.com/fe-guide/ko_PERFORMANCE