요약과 서평 | 프론트엔드 성능 최적화 가이드
사내 스터디에서 간단하게 읽어본 프론트엔드 성능 최적화 가이드에 대해 요약과 서평을 해본다.
요약
1장
페이지 표시 시간이 느려질수록 사용자 이탈률은 급격히 증가한다.
웹 성능을 결정하는 요소
- 로딩 성능
- 서버에 있는 웹 페이지와 웹 페이지에 필요한 기타 리소스를 다운로드할 때의 성능
- 렌더링 성능
- 다운로드한 리소스를 가지고 화면을 그릴 때의 성능
Web Vitals
- First Contentful Paint(FCP)
- 페이지가 로드될 때 브라우저가 DOM 콘텐츠의 첫 번째 부분을 렌더링하는 데 걸리는 시간에 관한 지표
- Speed Index(SI)
- 콘텐츠가 시각적으로 표시되는 속도를 나타내는 지표

- Largest Contentful Paint(LCP)
- 이미지가 로드될 때 화면 내에 있는 가장 큰 이미지나 텍스트 요소가 렌더링 되기까지 걸리는 시간을 나타내는 지표
- Time to Interactive(TTI)
- 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간을 측정한 지표
- Total Blocking Time(TBT)
- 페이지가 클릭, 키보드 입력 등의 사용자 입력에 응답하지 않도록 차단 된 시간을 총합한 지표
- 측정은 FCP와 TTI 사이의 시간 동안 일어난다
- Cumulative Layout Shift(CLS)
- 페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동을 측정한 지표
이미지 CDN
이미지 CDN을 활용하여 이미지 사이즈를 줄이거나 특정 포맷으로 변경 가능
텍스트 압축
HTML, CSS, 자바스크립트는 텍스트 기반의 파일
Network 패널 API Response Headers에 Content-Encoding으로 압축 여부 및 형식 확인 가능
종류 : Gzip, Deflate
2장
애니메이션 최적화
60hz : 1초에 60장의 화면을 새로 그림
display: none은 렌더 트리에 포함되지 않지만, opacity: 0이나 visibility: hidden인 요소는 렌더 트리에 포함됨

Reflow

Reflow가 발생하면 위 과정을 반복한다.
Repaint

하드웨어 가속을 사용하면 레이아웃 단계를 건너뜀
하드웨어 가속 : CPU에서 처리해야 할 작업을 GPU에 위임하여 더욱 효율적으로 처리하는 방법
transform, opacity 를 사용하면 요소를 별도 레이어로 분리하여 GPU로 보냄
transform: translate()는 처음부터 레이어를 분리하지 않고 변화가 일어나는 순간 레이어를 분리한다. 반면에 transform: translate3d() 또는 scale3d()와 같은 3d 속성들, 또는 will-change 속성은 처음부터 레이어를 분리해 둔다.
컴포넌트 지연 로딩
단점 : 필요해서 띄울 때 지연이 생길 수 있음
→ 사전 로딩으로 해결 가능
사전 로딩 예시
// 마우스를 올려놨을 때
const handleMouseEnter = () => {
const component = import('./components/ImageModal')
}
이미지 사전 로딩
const img = new Image();
img.src = '{이미지 주소}';
Image 객체는 위와 같이 new 연산자를 이용하여 생성, src 속성에 원하는 이미지의 주소를 입력하면 해당 이미지를 로드함
3장
Coverage 패널 : 각 파일의 코드가 얼마나 실행됐는지 비율 확인 가능
특정 파일에서 극히 일부의 코드만 실행되었다면, 즉 퍼센티지가 낮다면 해당 파일에 불필요한 코드가 많이 포함되어 있다고 볼 수 있음
InterSection Observer를 활용한 Img lazy Loading
function Card(props) {
const imgRef = useRef(null)
useEffect(() => {
const options = {}
const callback = (entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
console.log('is intersecting', entry.target.dataset.src);
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target)
}
})
}
const observer = new Intersectionobserver(callback, options)
observer.observe(imgRef.current)
return () => observer.disconnect()
}, [])
return (
<div className='Card text-center'>
<img data-src={props.image} ref={imgRef} />
<div className='생략'>{props.children}</div>
</div>
)
}
비트맵 이미지 포맷
PNG : 무손실 압축 방식으로 원본을 훼손 없이 압축하며 알파 채널을 지원하는 이미지 포맷
- 알파 채널: 투명도
JPG : PNG와는 다르게 압축 과정에서 정보 손실이 발생, 하지만 그만큼 이미지를 더 작은 사이즈로 줄일 수 있음
WebP : WebP는 무손실 압축과 손실 압축을 모두 제공하는 최신 이미지 포맷으로, 기존의 PNG나 JPG에 비해서 대단히 효율적으로 이미지를 압축 가능
사이즈 : PNG > JPG > WebP
화질 : PNG = WebP > JPG
호환성 : PNG = JPG > WebP
Picture 태그
picture 태그는 다양한 타입의 이미지를 렌더링하는 컨테이너로 사용된다. 브라우저 사이즈에 따라 지정된 이미지를 렌더링하거나 지원되는 타입의 이미지를 찾아 렌더링함
폰트 최적화
FOUT (Flash of Unstyled Text) : 폰트의 다운로드 여부와 상관없이 먼저 텍스트를 보여 준 후 폰트가 다운로드되면 그때 폰트를 적용하는 방식
FOIT (Flash of Invisible Text) : 폰트가 완전히 다운로드되기 전까지 텍스트 자체를 보여 주지 않음
대부분 브라우저는 3초만 기다리는 FOIT으로 설정되어 있다. (3초 동안은 폰트가 다운로드되기를 기다리다가 3초가 지나도 폰트가 다운로드되지 않으면 기본 폰트로 텍스트를 보여줌)
CSS의 font-display 속성을 이용하면 폰트가 적용되는 시점을 제어 가능
- auto : 브라우저 기본 동작 (기본값)
- block : FOIT (timeout = 3s)
- swap : FOUT
- fallback : FOIT (timeout = 0.1s) / 3초 후에도 불러오지 못한 경우 기본 폰트로 유지, 이후 캐시
- optional : FOIT (timeout = 0.1s) / 이후 네트워크 상태에 따라 기본 폰트로 유지할지 결정, 이후 캐시
폰트 포맷 파일 크기
- EOT > TTF/OTF > WOFF > WOFF2
서브셋 폰트
ex) 이진수 binary ← 해당 문자에 대한 폰트 정보만 가져온다. 당연히 가볍다
Base 64로 변환해서 src: url('') 에 넣으면 아주 빠르다. ← 항상 좋은 건 아니다. (data-url이 포함된 만큼 CSS 파일의 다운로드가 느려질 것이다.)
캐시 최적화
- 메모리 캐시 : 메모리에 저장하는 방식, 여기서 메모리는 RAM을 의미한다.
- 디스크 캐시 : 파일 형태로 디스크에 저장하는 방식
Cache-Control
- no-cache: 캐시를 사용하기 전 서버에 검사 후 사용
- no-store: 캐시 사용 안 함
- public: 모든 환경에서 캐시 사용 가능
- private: 브라우저 환경에서만 캐시 사용, 외부 캐시 서버에서는 사용 불가
- max-age: 캐시의 유효 시간
Etag 값 비교해 캐시된 리소스와 서버의 최신 리소스가 같은지 체크한다.
일반적으로 HTML 파일은 no-cache 설정을 적용한다. (항상 최신 버전의 웹 서비스를 제공하기 위해서, HTML이 캐시되면 캐시된 HTML에서 이전 버전의 자바스크립트나 CSS를 로드하게 되므로 캐시 시간 동안 최신 버전 의 웹 서비스를 제공하지 못함)
빌드된 자바스크립트와 CSS는 파일명에 해시를 가지고 있기 때문에 코드가 변경되면 해시도 변경되어 완전히 다른 파일이 되어버린다. 따라서 캐시를 아무리 오래 적용해도 HTML만 최신 상태라면 자바스크립트나 CSS 파일은 최신 리소스를 로드할 수 있다.
4장
앞선 Chapter에 중복되는 내용들이 있어서 짧게 정리한다.
이미지 지연 로딩
react-layload Library 사용
<ImageWrap>
<LazyLoad offset={1000}>
<Image src={/* 생략 */} alt={alt} onClick={openModal} />
</LazyLoad>
</ImageWrap>
CLS 피하기
aspect-ratio , padding 등을 활용하여 CLS를 피할 수 있다.
병목 코드 최적화
함수 메모이제이션 적용
const cache = {};// 함수의 반환 값을 저장하기 위한 변수
function square(n) {
if (cache[n]) { // 이미 저장된 값이라면 기존 값을 그대로 반환
return cache[n];
}
//
const result = n * n;
cache[n] = result; // 다음에 저장된 값을 사용할 수 있도록 저장
return result;
}
const cache = {};
export function getAverageColorOfImage(imgElement) {
if (cache.hasOwnProperty(imgElement.src)) {
return cache[imgElement.src];
}
/* 중략 */
cache[imgElement.src] = averageColor;
return averagecolor;
}
서평
스터디원들과 마지막 간단한 서평을 했는데 모두가 빠르고 가볍게 읽을 수 있는 책이었다고 평해주었다. 나도 그 부분에 공감했다. 요즘 일이 많아 정신이 없었는데 어렵고 고민을 많이 해야 할 책이었다면 오히려 진도를 못 나갔을 것이다.
하지만 책이 내용 자체가 가볍다는 말은 아니다. 요약본과 책을 빠르게 다시 한번 보니 내용은 프론트엔드 개발자들이 꼭 알아야 하는 내용들로 꽉꽉 차있다. (아무래도 스터디원들 연차가 3~4년 차들이라 블로그나 각자 따로 본 적이 있는 내용들이 있어서 가볍고 빠르게 읽을 수 있다고 생각한 것 같다.)
내가 모르는 부분도 새롭게 알게 되어 좋았고, 지식만 알고 자주 사용하지 않으면 휘발되는 내용을 책을 통해 다시금 읽어보며 리마인드할 수 있는 좋은 시간이었다!
추천!