-
로딩 성능 최적화 수업 정리Web/Optimization 2021. 9. 22. 19:38반응형
#1. 요청 크기 줄이기
어떤 타입의 요청이 크기가 큰지를 파악하고, 해당 요청의 크기를 줄인다.
1. 텍스트 컨텐츠 (소스 코드)
■ Minify & Uglify
- 번들러에서 빌드 옵션으로 제공
- Webpack 5부터 production mode에서는 기본으로 최적화
- CSS 파일은 별도로 minify
- CSS in JS는 Babel transpile 과정에서 minify
■ GZIP 압축
- 모던 브라우저에는 모두 내장되어있는 압축 프로그램
- AWS, 스프링 등 서버에서 설정할 수 있음
- Brotli는 더 효율적으로 압축, 그러나 아직까진 지원이 미비
2. 이미지
평균적으로 웹페이지 용량에서 이미지가 파지하는 비율은 60% 이상.
이미지 용량만 잘 관리해도 어느 정도의 성능은 잡고갈 수 있다.
■ 이미지 해상도가 화면에 보이는 크기보다 크네?
- Image resize
- webpack에서 image resize loader를 사용하는 방법
- 서버에서 처리해주는 방법
- Google squoosh 등을 이용해 직접 resize해주는 방법
■ 모바일에선 이렇게까지 큰 이미지가 필요 없는데?
- srcset을 활용한 '반응형 이미지(Responsive image)'
■ 해상도를 잘 맞췄는데도 용량이 크네?
- 이미지 포맷(Image format) 점검
ο PNG
- 무손실 포맷
- 투명도 지원
- JPEG에 비해 5~10배까지도 커질 수 있음
- PNG-8, PNG-24에 따라 용량 차이
ο JPEG
- 손실 압축 방식
- 투명도 지원 X
- 상대적으로 작은 크기
- Progressive JPG: 처음에 저화질로 렌더링 후 점진적 화질 개선
- CPU 자원을 좀 더 소모한다.
- 모바일 사파리에서는 지원 미비.
참고: https://www.airpair.com/ios/posts/loading-images-ios-faster-with-progressive-jpegs
ο WebP
- 손실/무손실 압축 모두 지원
- 손실 압축에서 JPEG보다 25~34%, 무손실 압축에서 PNG보다 26% 작은 크기
- 브라우저 지원 범위 확인 필요
ο GIF
- 무손실 포맷
- 대부분 매우 큰 용량
- 애니메이션, 투명도 지원
ο HEIF / HEIC (High Efficiency Image Format / Container)
- 고효율, 즉 저용량 고품질
- 애플 전용
- 이미지 압축(Image compress)
- 손실 압축(lossy comperssion) vs 무손실 압축(lossless compression)
- 이미지 메타 정보 제거(EXIF 등)
- 검색어: 'remove image metadata', 'remove image exif'
이미지 품질과 성능 사이에서 항상 균형점을 찾기 위해 고민해야한다.
사용자 경험을 개선하기 위해 이미지를 압축하는건데, 깨진 이미지가 사용자 경험을 해칠 수 있기 때문.
3. 폰트
■ 폰트 구성요소 중 필요 없는 게 있나?
- font-weight
- subset: 사용하지 않는 글자들을 제거한 서브셋을 사용
- '뷁'과 같은 잘 사용되지 않는 글자
- CJK(China, Japan, Korea)에서 더 효과적
참고자료 1. 웹 폰트 최적화 하기
참고자료 2. 웹 폰트의 사용과 최적화의 최근 동향
#2. 필요한 것만 필요한 때에 요청하기
- HTTP의 브라우저 호스트 당 최대 Connection 수 제한
- 브라우저는 하나의 호스트 당 동시에 맺을 수 있는 Connection 수를 제한하고 있다.
- 브라우저마다 제한 갯수는 다르지만, 대부분의 최신 브라우저들은 호스트 당 최대 6개의 연결을 지원한다.
- HTTP/1.1 기준.
※ Domain Sharding
여러 개의 서브 도메인을 생성하여 정적 파일을 병렬로 가져옴으로써, 정적 파일의 로딩 속도를 개선하는 방법.
동시 요청으로 속도가 무작정 빨라질 것 같지만, DNS 조회로 인해 꽤 많은 시간과 CPU, 전력을 소모한다.
따라서 Domain Sharding은 더 이상 동시요청에 대한 좋은 방법이 아니고, HTTP/2가 더 나은 대안.
참고: https://wonism.github.io/domain-sharding/
※ HTTP/2
HTTP/1.1에 비해 성능 개선에 초점을 맞춰 개발된 프로토콜.
구글이 HTTP/2 개발에 참여하게 되면서, 구글의 SPDY 프로토콜이 자연스레 녹아들었다.
HTTP/2는 Multiplexing이 지원되기 때문에 호스트 당 최대 연결 제한이 풀려있다.
Multiplexing을 이용하면 브라우저는 하나의 TCP 연결을 이용하여 여러 개의 데이터 요청을 보낼 수 있고 요청 순서에 상관없이 응답을 받을 수 있게 된다. 그러면 앞선 요청이 끝날 때까지 기다리지 않아도 될 뿐더러, 여러 개의 연결을 생성하는 데에 필요한 오버헤드도 생략될 수 있다.
HTTP/2에 추가된 기능들 중에 Server Push라는 기능도 있는데, 이를 이용하면 서버가 클라이언트의 요청 하나에 여러 개의 응답을 할 수가 있게 된다. 클라이언트의 요청을 받아 응답할 때 함께 푸시해주고 싶은 리소스의 목록을 함께 전달해서 클라이언트가 불필요한 요청을 하지 않게끔 도와줄 수 있다.
참고: https://ibocon.tistory.com/257?category=879638
https://stackoverflow.com/questions/36517829/what-does-multiplexing-mean-in-http-2
AWS에서는 아마 기본적으로 HTTP/2가 적용될텐데, Spring이나 그런 것들과 함께 사용되는 경우에는 버전을 따로 명시해줘야 할 때도 있다고 하니, 잘 살펴보고 적용할 수 있도록 하자.
1. 요청 수 줄이기
■ 요청은 하는데 쓰이는 데가 없다면?
- 불필요한 리소스 요청이 있는지 점검하기
- Tree Shaking(실제 코드 상에서는 쓰이지 않는 코드들을 제거)
- 실제로는 사용되지 않는데 쓸데없이 빌드 결과물에 포함되는 코드 제거하기
- import 최적화
■ 지금 보이는 화면에서 당장 필요하지 않다면?
- Dynamic Import
- Lazy Loading & intersectionObserver
- (React) Route 별 Code Splitting (React Lazy, Suspense)
■ 이미지를 너무 자잘자잘하게 여러 번 가져온다면?
- Image Sprite
- 여러 개의 이미지를 하나로 합쳐서 관리.
- 사용할 때는 CSS 속성을 이용해 background 좌표를 옮겨서 원하는 이미지를 적용
- 이미지를 캐싱할 때 전체 이미지 중 아이콘 하나만 변경되어도 전체 이미지가 캐싱 무효화되는 단점.
- 이미지 리더가 제대로 읽지 못하는 탓에 접근성 문제도 존재.
- 다른 방법으로 이미지 대체하기
ο data URI로 대체하기
- 텍스트 포맷으로 이미지 노출
- HTML 내부에서 스크립트처럼 함께 포함될 수 있다.
- decoding에 브라우저 리소스가 소모된다. 권장하는 방식은 아님.
- 번들 사이즈가 커질 수 있다.
ο CSS로 대체하기
- 브라우저, OS 등에 따라 폰트가 전부 달라지는 경우도 많다.
- 그래서 폰트같은 것들을 이미지화 시켜서 이미지 스프라이트에 포함시키는 경우가 많다.
- 이런 부분들을 CSS 대체할 수 있다면 최대한 대체하는 것도 방법.
ο SVG로 대체하기
- 역시나 너무 남용하면 번들 사이즈가 커진다.
2. 브라우저 리소스 우선순위 조정하기
■ 바로 필요하니까, 미리 가져와야할 것 같은데?
ο preload
<link rel="preload" href="/style.css" as="style" />
- 리소스 우선순위: 높음
- 현재 페이지에서 바로 필요한 리소스. 빠르게 가져와야한다고 브라우저에게 알려줌
- 바로 받아오되(fetch), 바로 실행(execute)하지는 않는다.
- 스크립트를 미리 받아와도 실행은 나중에 하는 것처럼
- onload 이후 3초 이내에 preload한 리소스를 사용하지 않을 경우 크롬에서는 경고 로그가 발생
- 받아온 뒤 브라우저에 캐시된다.
- 예시) 폰트, 초기 렌더링(Critical Rendering Path)에 반드시 필요한 리소스 등
ο prefetch
<link rel="prefetch" href="/chunk.js" as="script" />
- 리소스 우선순위: 낮음
- 브라우저에게 미래에 필요할 것 같은 페이지 혹은 리소스(script, css 등)를 미리 다운받으라고 알려줌.
- 현재 페이지가 다 로드된 이후 후순위로 다운받아 prefetch cache에 넣어둔다.
- 현재 페이지의 로드 시간에는 영향을 미치지 않음.
- 다음 navigation(페이지 이동)의 FCP나 TTI에 영향
- 예시) 동일한 앱의 다른 페이지에서 사용되는 JS 번들, 현재 페이지에서 쓰이지만 초기 렌더링에는 필요 없는 리소스(ex. 모달), 페이징된 목록에서 다음 페이지의 컨텐츠 등
ο preconnect
<link rel="preconnect" href="https://example.com" /> <link rel="preconnect" href="https://cdn.example.com" />
- 브라우저에서 서버와 연결만 미리 맺어두고 있으라고 알려주는 것
- connection 미리 맺어두기로 개별 요청당 100 ~ 500ms 정도의 로드 시간을 절약
- 다운로드는 미리 하지않고 connect만 미리 해두기 때문에 딱 연결 시간만큼만 단축된다.
- 예시) 외부 도메인 레소스(ex. 구글 폰트)
참고: https://www.airpair.com/ios/posts/loading-images-ios-faster-with-progressive-jpegs
■ 이건 바로 필요없을 것 같아.
자바스크립트는 파서 차단 리소스(Parser blocking resource)이다.
ο defer
- script fetch, script execution 모두 HTML parsing에 영향을 끼치게 하고 싶지 않을 때.
- script fetch는 HTML parsing과 함께 비동기로 이루어진다.
- HTML parsing이 끝날 때까지 스크립트 실행을 지연한다.
- defer script fetch > DOMContentLoaded > defet script execute
ο async
- script fetch만 HTML parsing이랑 상관없이 하고싶을 때.
- script execution은 script fetch만 다 되면 당장에라도 HTML parsing 중간에 일어날 수 있다.
- DOMContentLoaded, 혹은 다른 스크립트들과 독립적으로 동작한다.
- 예시) Google Analytics, 광고 등. 문서 내용이나 앱 자체와는 관련없이 독립적으로 동작하는 스크립트
참고: https://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html
#3. 같은 건 매번 새로 요청하지 않기
■ HTTP Caching
캐싱(Caching)은 리소스의 사본을 저장해두었다가 같은 요청이 들어왔을 때 저장해 둔 사본을 제공하는 것을 의미한다. 브라우저도 자체적으로 캐시를 가지고 있는데, 브라우저 캐시에 있는 리소스가 유효한 리소스이면 서버에 요청 자체를 보내지 않는다.
- 캐시는 HTTP Response Header에 정해진 값에 따라 세팅된다.
ο 브라우저가 서버에 요청하지 않고, 캐싱된 자원을 바로 사용하도록 하고싶을 때
- 리소스를 요청받았을 때 서버는 응답으로 주는 리소스의 유효 기간을 설정할 수 있다.
- Cache-Control: 클라이언트에 어떻게 캐싱할 지 지정. max-age에 유효기간을 초 단위로 지정한다.
- Expires: 절대 유효기간 지정(deprecated)
- max-age 설정은 평균적으로 최대 1년 = 31,536,000초
- 지정된 캐시 유효 기간 내에 다시 요청하면 서버에 요청하지 않고 캐시된 리소스 사용
※ no-cache, no-store
리소스를 요청받았을 때 서버는 응답으로 주는 리소스를 캐시하지 말라고 알려줄 수 있다.
- Cache-Control: no-store
- 캐시 불가능. 매번 서버에서 새로 받아와야한다.
- Cache-Control: no-cache
- 캐시 가능하긴 하지만, origin 서버에 매번 캐시된 리소스의 유효성 검증 요청
- 유효성 검증은 연결 비용은 동일하지만, 받아오는 데이터는 없고 유효성 여부만 받아오므로 좀 더 가볍다.
※ Cache-Control: must-revalidate
만료된 캐시만 서버에 검증을 받도록 한다.
즉, max-age에 아직 도달하지 않았다면 캐시된 리소스를 사용하고, 그렇지 않다면 재검증을 한다.
참고: https://yceffort.kr/2020/10/http-cache
https://stackoverflow.com/questions/18148884/difference-between-no-cache-and-must-revalidate
ο 브라우저가 캐싱된 자원이 최신이 맞는지 서버에게 추가로 확인해야할 때: 조건부 요청(Conditional Request)
- 캐시를 읽어왔는데 유효기간이 지났다면(stale), 클라이언트는 서버에 캐시에 있는 걸 써도 되는지 신선도 재검사(freshness revalidation) 요청을 보낸다.
- 받았던 응답 헤더에 ETag가 있었다면
- If-None-Match 요청 헤더에 캐시의 ETag 값을 넣어 서버의 ETag와 같은지 비교(ETag가 바뀌었으면 업데이트 해야하는 리소스라는 의미)
- 받았던 응답 헤더에 Last-Modified가 있었다면
- If-Modified-Since에 캐시의 Last-Modified 값을 넣어 서버에서 그 이후로 수정이 있었는지 확인
- 브라우저 캐시 용량
- 대부분의 최신 브라우저들은 default disk cache size를 아주 작게 세팅한다.
- 파이어폭스는 50MB, IE는 8 ~ 50MB, 크롬은 80MB 이하
- Disk Cache와 Memory Cache의 차이
- Memory Cache는 RAM에 저장되기 때문에 읽기와 쓰기에 빠른 반면, 컴퓨터가 종료될 때 휘발된다.
- Disk Cache는 하드 드라이브에 저장되기 때문에 읽기 쓰기가 느린 반면 디스크에 항상 저장되어있다.
참고: https://toss.tech/article/smart-web-service-cache
https://imagekit.io/blog/ultimate-guide-to-http-caching-for-static-assets/
■ CDN(Contents Delivery/Distribution Network)
컨텐츠 전송 네트워크. 클라이언트가 원본 서버와 지리적으로 거리가 있는 경우, 보다 효율적으로 데이터를 전달하기 위해 클라이언트에 가까운 지점의 서버, 즉 CDN에 원본 서버의 컨텐츠를 캐시한다.
ex) CloudFront, CloudFlare 등
ο Cache-Control: public
- Cache-Control: public,s-maxage=31536000,max-age=0
- 중간 프록시(ex. CDN)에 캐시를 저장할 수 있다.
- s-maxage: 프록시와 같은 공유(public) 캐시에만 적용되는 유효기간
ο Cache-Control: private
- 최종 끝의 클라이언트만 캐시 가능
- CDN에는 캐싱이 안된다.
default는 아마 private이므로, CDN 캐싱을 이용하고 싶다면 public을 지정해줄 것.
■ Cache Invalidation
유효기간이 아직 끝나지 않았지만 클라이언트가 캐시된 리소스를 사용하지 않고 서버에서 갱신된 최신 리소스를 사용하도록 강제해야 한다면
- 브라우저 캐시 무효화는 불가능하다. (사용자가 직접 캐시를 제거하지 않는 이상)
- CDN 캐시 무효화 기능은 해당 플랫폼에서 제공(ex. CloudFront의 Invalidation)
■ 정적 리소스에 고유한 값 붙여서 쓰기(캐시 날리기)
- content hash / chunk hash
- 라이브러리 코드같은 것들은 변하지 않으므로, 따로 chunk로 분리해서 캐싱하기도 한다.
- 키워드: 'cache busting'
■ API Cache
- React Query, SWR
반응형'Web > Optimization' 카테고리의 다른 글
렌더링 성능 최적화 수업 정리 (0) 2021.09.24 이미지 프리로딩(Image Preloading) (2) 2021.09.23 프론트엔드 웹 로딩 최적화 (2) 2021.08.16