같은 건 매번 새로 요청하지 않기
캐시(cache)는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다.
리소스의 사본을 저장해두었다가 같은 요청이 들어왔을 때 저장해둔 사본을 제공하는 것
캐시를 적절하게 사용하면 동일한 리소스에 대한 불필요한 작업을 줄일 수 있어 경제적이다. 하지만 잘못 사용하면 올바르지 않은 오래된 값을 가져와 부작용을 야기할 수 있으므로 적절하게 캐시를 설정하는 것이 중요하다.
HTTP Cache
캐시는 크게 Shared(공유) 캐시와 Private(사설) 캐시가 있다. 캐시는 요청을 보내는 클라이언트에서 보관할 수도 있고, 클라이언트와 서버 사이의 프록시 서버 등의 중개 서버에서 보관할 수도 있다. private 캐시는 단일 사용자가 사용하고, shared 캐시는 여러 사용자에 의해 재사용 되는 응답을 캐싱한다. 주로 서버에서 shared 캐시를 보관한다. Cache-Control 정책은 기본적으로 private이다.
HTTP 캐싱과 관련해서 가장 중요한 부분은 응답 헤더의 필드들이다. Cache-Control, ETag, Last-Modified 필드들은 캐싱에 영향을 미친다. 이 중 Cache-Control에 대해서 알아보자.
Cache-Control: max-age=31536000
Cache-Control: s-maxage=31536000
max-age와 s-maxage는 리소스가 fresh한 시간을 초(second)로 나타내는 것이다. 설정할 수 있는 범위는 0에서 31536000(1년)이다. s-maxage의 경우 공유 캐시에서만 사용되고, max-age와 함께 사용시 max-age는 무시된다. Expires(유효기간 지정)과 동시에 설정되어 있다면 Cache-Control이 우선시 된다. 응답 값의 헤더의 Age값과 비교하여 계산된다.
Cache-Control값이 생략되더라도 HTTP캐싱이 비활성화 되지 않는다. 브라우저는 어떤 유형의 캐싱이 가장 적절한지 추측한다(Heuristic Freshness. firefox의 경우 min(1 week, (current time - last modified time) / 10)로 계산된다)
Cache-Control: no-cache
캐시하지 않음(매번 서버에서 새로 받아오기)으로 오해하면 안된다. 캐시를 사용하기 전에 서버에게 이 캐시가 유효한지 확인하도록 하는 설정이다. 캐시를 하지 않고 싶다면 no-store로 설정해줘야 한다.
재검증
캐시를 읽어왔는데 신선하지 않다면(stale) 클라이언트는 서버에 캐시에 있는 것을 써도 되는지 신선도 재검사(freshness revalidation)요청을 보낸다. 헤더에 If-Modified-Since와 If-Match를 설정하여 조건부 요청을 전송한다.
If-Modified-Since는 Last-Modified를 확인하여 서버에서 그 이후로 수정이 있었는지 확인한다. ETag는 리소스가 바뀌었는지 확인하는 식별자이다. If-None-Match 요청 헤더에 캐시의 ETag 값을 넣어 서버의 ETag와 같은지 비교한다.
리소스가 변경되지 않았다면, 서버는 304(Not Modified)응답을 전달한다. 이는 캐시를 다시 fresh하게 만들어주며 클라이언트는 그 리소스를 사용하게 된다.
리소스가 변경되었다면, 서버는 새로운 리소스와 함께 200 OK 응답을 돌려준다.
💡 max-age=0
정의대로라면 max-age=0 값이 Cache-Control 헤더로 설정되었을 때, 매번 리소스를 요청할 때마다 서버에 재검증 요청을 보내야 한다. 그렇지만 일부 모바일 브라우저의 경우 웹 브라우저를 껐다 켜기 전까지 리소스가 만료되지 않도록 하는 경우가 있다. 네트워크 요청을 아끼고 사용자에게 빠른 웹 경험을 제공하기 위해서이다.
CDN(Contents Delivery Network, Contents Distribution Network)
지리적으로 분산된 여러개의 서버이다. 컨텐츠를 사용자와 가까운 곳에서 전송함으로써 전송 속도를 높인다. 사용자가 해당 서버에서 멀리 떨어져 있는 경우 동영상 또는 웹 사이트 이미지와 같은 대용량 파일을 로드하는 데 시간이 오래 걸린다. 지리적으로 사용자와 가까운 CDN 서버에 저장하면 사용자에게 훨씬 빨리 도달할 수 있다.
1️⃣ 사용자가 정정 웹 콘텐츠를 처음으로 요청한다
2️⃣ 요청이 오리진 서버에 도달한다. 오리진 서버는 응답을 보낸다. 또한, 해당 사용자와 지리적으로 가장 가까운 CDN에 응답 복사본을 보낸다.
3️⃣ CDN 서버는 복사본을 캐싱된 파일로 저장한다.
4️⃣ 다음에 해당 사용자 또는 해당 위치에 있는 다른 사용자가 동일한 요청을 하면, 오리진 서버가 이닌 캐싱 서버가 응답을 보낸다.
CloudFront(CDN) cache 설정하기
CDN에서 보관되는 캐시 역시 수명이 존재한다. 캐시 정책, 원본 요청 정책, 응답 헤더 정책을 설정할 수 있다.
캐시 정책: 캐시와 압축 설정. TTL(Time To Live)과 캐시키를 설정한다. 따로 설정해 주지 않으면 Default TTL로 설정된 시간을 기준으로 캐시되며, Minimum TTL과 Maximum TTL의 범위를 벗어나면 강제로 범위 안의 값을 사용하도록 캐시 수명을 설정한다. 캐시키는 캐시 객체의 식별자이다. 요청을 받으면 CloudFront는 이 캐시키를 통해 적중 여부를 확인한다.
원본 요청 정책: 캐시 누락이 있을 때 CloudFront에서 원본으로 보내는 요청. 오리진 요청에 포함될 정보를 포함할 수 있고, 사용자 요청에 포함되지 않은 특정 HTTP 헤더를 오리진 요청에 추가할 수 있다.
응답 헤더 정책: 클라이언트에게 전달하는 응답 헤더를 덮어쓰기(override). 오리진에서 설정해주지 않은 항목까지 지정해 줄 수 있다. 오리진 재정의 설정을 하게 되면 오리진에서 동일한 값을 설정했더라도 이 설정으로 덮어씌워진다. 따라서 클라이언트로는 이곳에서 설정한 값대로 설정된다.
Cache-Control 어떻게 구성할까?
- 사용하기 전에 서버에서 재검증해야 하는 리소스의 경우 Cache-Control: no-cache
- 캐싱되지 않아야 하는 리소스의 경우 Cache-Control: no-store
- 버전 지정된 리소스의 경우 Cache-Control: max-age=31536000
참고
https://developer.mozilla.org/ko/docs/Web/HTTP/Caching
https://web.dev/i18n/ko/http-cache/
https://developer.mozilla.org/ko/docs/Web/HTTP/Caching
https://developer.mozilla.org/ko/docs/Web/HTTP/Conditional_requests
https://aws.amazon.com/ko/what-is/cdn/
https://toss.tech/article/smart-web-service-cache
최소한의 변경만 일으키기
브라우저의 Main Thread가 할 수 있을 정도의 적정 수준의 일만 하게 하도록 해서 page junk없이 60fps를 유지해보자. 스크롤이나 애니메이션과 같은 것들을 부드럽게 보이게 하려면, 초당 60프레임(1 프레임당 16.67ms, paint를 위한 시간을 고려한다면 약 10ms)가 필요하다.
브라우저 렌더링 과정
HTML을 파싱하여 DOM 트리를 만들고, CSS를 파싱하여 CSSOM 만든다. 그리고 이 두 트리를 합쳐 Render트리를 만든다.
Layout 과정에서는 Render Tree를 재귀적으로 돌면서 새롭게 생기거나 업데이트된 부분이 없는 지 확인하며 각 노드의 위치를 잡는다. 이러한 Layout 과정은 꽤나 expensive하기 때문에 불필요한 경우에는 계산을 하지 않기 위해 브라우저는 cache를 사용한다. 이후 paint 과정에서 이를 브라우저에 그리게 된다.
painting & composite
렌더링의 과정을 크게 parse, style, layout 과정과 paint, composite과정으로 나눌 수 있다. paint과 composite 단계에서는 이전 단계를 화면에 그리게 된다. 브라우저의 메인 쓰레드는 DOM, 레이아웃 및 자바스크립트 실행 뿐만 아니라 painting과 composite도 담당한다. 메인 쓰레드가 paint 및 composite을 하는 동안에는 다른 작업을 할 수 없다. 메인 쓰레드가 너무 바빠서 GPU로 옮길 수 있는 paint와 composite과정을 GPU로 옮기기 시작했다. 브라우저 렌더링 과정에 대한 자세한 사항은 이글에서 너무 잘 설명해주어 첨부한다.
reflow, repaint 줄이기
reflow는 브라우저가 elemenet의 위치를 다시 계산해야 할 때 일어난다. repaint는 paint과정이 다시 일어날 때 일어난다. 이 과정들을 상대적으로 expensive하기 때문에 이들을 유발하는 속성을 줄이고 layer를 사용하여 가능한 composite이 바로 일어날 수 있게 변경해보자.
Layout Shift 줄이기
layout shift는 엘리먼트가 주변 컨텐츠에 영향을 끼치면서 위치나 사이즈를 바꿀때 일어난다. 의도하지 않은 layout shift는 사용성에 문제를 일으키기 때문에 이를 줄이는 것이 좋다. layout shift 측정은 크롬 개발자 도구 -> performance에서 할 수 있다.
- 이미지 사이즈 지정해두기. width, height를 지정하기. 혹은 aspect-ratio를 통해 비율 정해놓기.
- 레이아웃 이동을 유발하는 css 피하기. 레이아웃의 이동을 유발하는 속성보다는 transform을 사용하는 것이 좋다.
.sidebar {
will-change: transform;
}
- will change. 브라우저에게 이 값이 바뀔거라는 것을 미리 알려주기. 브라우저는 동적으로 변화할 값을 알고 더 부드러운 이벤트를 구사할 수 있다. 다만 과도한 will-change는 페이지 속도를 늦추거나 많은 자원을 소비할 수 있으므로 정말 필요할 때만 사용하는 것이 좋다.
단순히 layer를 늘리는 것이 꼭 좋은 방법은 아니다. 레이어들은 많은 메모리를 차지하고 실제로 작업을 더 느리게 만들 수 있다.
gpu에게 일감을 넘겨주는 기준은 다음과 같다. 단순히 css를 수정하는 것만으로 렌더링 최적화를 이룰 수 없음을 주의하자
1. <video> 혹은 <canvas> 태그 사용
2. 3D transform 요소 적용
3. animation 이나 transition 사용
4. will-change로 opacity, transform, top, left, bottom, right등이 정의된 경우
5. iFrame일 경우
6. flash와 같은 일부 플러그인
7. backface-visibility 가 hidden일 경우
React.memo
라이브러리의 도움을 받아 이미 그려진 화면을 다시 그리지 않도록 해보자. React의 memo를 사용하여 불필요한 리렌더링을 줄일 수 있다. 적절한 곳에 memo를 도입하고 React Dev Tools의 Profiler를 통해 확인하면 된다.
참고
https://www.webperf.tips/tip/browser-rendering-pipeline/
https://www.webperf.tips/tip/event-loop/
https://ui.toast.com/weekly-pick/ko_20171016
'우테코' 카테고리의 다른 글
에러 처리 방식에 대한 고민 (0) | 2022.10.30 |
---|---|
Refresh Token 도입기 (0) | 2022.10.23 |
프론트엔드 성능 최적화 - 1 (0) | 2022.09.06 |
내편 UI 개발기 (6) | 2022.09.02 |
[Level 2] 미션 1: 페이먼츠 1, 2단계 (0) | 2022.05.16 |