[북리뷰] 프론트엔드 성능 최적화 가이드 정리(#2)

하수도키

·

2023. 2. 9. 14:58

728x90
반응형
SMALL

2023.02.09 - [책일기] - [북리뷰] 프론트엔드 성능 최적화 가이드 정리(#1)

 

[북리뷰] 프론트엔드 성능 최적화 가이드 정리(#1)

크롬 LightHouse Mode와 Categories 항목 값 소개 Mode Navigation: Lighthouse의 기본값으로, 초기 페이지 로딩 시 발생하는 성능 문제를 분석 Timespan: 사용자가 정의한 시간 동안 발생한 성능 문제를 분석 Snapsho

hasudoki.tistory.com

이미지 파일

SVG와 같은 벡터이미지가 아닌 비트맵 이미지 포맷들

  • PNG
  • JPG(JPEG)
  • WebP

PNG는 무손실 압축 방식

원본 훼손하지 않고 알파 채널을 지원

알파채널이란 투명도 의미

JPG는 PNG와 다르게 압축 과정에서 정보 손실이 발생

그만큼 이미지를 더 작은 사이즈로 줄일 수 있다.

일반적으로 고화질이나, 투명도 정보가 필요한게 아니면 JPG사용

WebP는 무손실 압축과 손실 압축을 모두 제공 최신 이미지 포맷

PNG대비 26%, JPG대비 25-34%

하지만, 최신 이미지 파일 포맷이라 모든 브라우저에서 사용이 쉽지 않다.

이미지 파일 요약

  • 사이즈 : PNG > JPG > WebP
  • 화질 : PNG > WeebP > JPG
  • 호환성 : PNG = JPG > WebP

이미지 변환 사이트

Picture Tag

# 뷰포트에 따라 구분
<picture>
  <source media="(min-width: 650px)" srcset="img_pink_flowers.jpg" />
  <source media="(min-width: 465px)" srcset="img_white_flowers.jpg" />
  <img src="img_orange_flowers.jpg" alt="Flowers" style="width: auto;" />
</picture>

# 이미지 포맷에 따라 구분
<picture>
  <source srcset="photo.avif" type="image/avif" />
  <source srcset="photo.webp" type="image/webp" />
  <img src="photo.jpg" alt="photo" />
</picture>

example
const callback = (entries, observer) => {
	entries.forEach(entry => {
		if(entry.isIntersecting) {
			const target = entry.target
			const previouseSibling = target.previousSibling

			console.log('is intersecting', entry.target.dataset.src)
			target.src = target.dataset.src
			previousSibling.srcset = previousSibling.dataset.srcset
			observer.unobserve(entry.target) // 한번 로드한 이미지는 다시 호출할 필요 없음
		}
	})
}

return (
		<div>
			<picture>
				<source data-srcset={props.webp} type="image/webp" />
				<img data-src={props.image} ref={imgRef} />
			</picture>
			<div className='생략'>{props.children}</div>
		</div>
	)

동영상 최적화

<video autoPlay loop muted>
    <source src={video_webm} type="video/webm" />
    <source src={video} type="video/mp4" />
  </video>
  • 화질이 많아 떨어져 보기 어려울 경우 패턴이나 블러 처리

폰트 최적화

폰트의 변화로 발생하는 현상은

FOUT(Flash Of Unstyled Text)

FOIT(Flash Of Invisible Text)

 

FOUT는 Edge 브라우저에서 폰트를 로드하는 방식

폰트의 다운로드 여부와 상관없이 먼저 텍스트를 보여 준 후 폰트가 다운로드되면 그때 폰트를 적용하는 방식

 

FOIT는 크롬, 사파리, 파이어폭스 등에서 폰트를 로드하는 방식

폰트가 완전히 다운로드되기 전까지 텍스트 자체를 보여주지 않는다.

 

하지만, 크롬에서 제대로 다운로드 되지 않았는데도 텍스트가 보인다.

그 이유는 완전한 FOIT가 아니라 3초만 기다리는 FOIT이기 떄문이다.

 

즉 3초동안은 폰트가 다운로드되기를 기다리다가

3초가 지나도 폰트가 다운로드되지 않으면 기본 폰트로 텍스트를 보여 줍니다.

 

FOUT VS FOIT

 

FOUT, FOIT는 어떤것이 더 좋다 할 수 없다 상황에 따라 써야 한다.

중요한 텍스트(뉴스 제목 등)의 경우 FOUT를 써야 한다. 중요한 단어가 보이지 않아 제목을 빠르게 보고 넘길 경우 오해가 있을 수 있다.

font-display @font-face 에서 설정 가능하다..

  • auto: 브라우저 기본 동작 (기본 값)
  • block: FOIT (timeout =3s)
  • swap: FOUT
  • fallback: FOIT (timeout = 0.1s) / 3초 후에도 불러오지 못한 경우 기본 포폰트로 유지, 이후 캐시(폰트 불러오면 캐시하고 다음에 적용)
  • optional: FOIT (timeout = 0.1s) / 이후 네트워크 상태에 따라 기본 폰트로 유지할지 결정, 이후 캐시
@font-face {
	font-family: BMYEONSUNG;
	src: url('./assets/fonts/BMYEONSUNG.ttf');
	font-display: fallback;
}
  • fallback을 사용해 FOUT 적용하면 갑자기 텍스트가 나오게 된다. 이 부분을 애니메이션 효과를 줘서 부드럽게 해보자.
  • 그러러면 폰트가 다운로드 완료 시점을 알아야 한다.
  • fontfaceobserver 라이브러리 사용해서 알 수 있다.
import FontFaceObserver from 'fontfaceobserver'

const font = new FontFaceObserver('BMYEONSUNG')

function BannerVideo() {
	useEffect(() => {
		font.load(null, 20000).then(function () {
			console.log('BMYEONSUNG has loaded')
		})
	}, [])

	// 생략
} 

실 사용 예제

import FontFaceObserver from 'fontfaceobserver'

const font = new FontFaceObserver('BMYEONSUNG')

function BannerVideo() {
	const [isFontLoaded, setIsFontLoaded] = useState(false)

	useEffect(() => {
		font.load(null, 20000).then(function () {
			console.log('BMYEONSUNG has loaded')
			setIsFontLoaded(true)
		})
	}, [])

	// 생략
	return (
		<div>
			{/* 생략 */}
			<div
				style={{ opacity: isFontLoaded ? 1 : 0, transition: 'opacity 0.3s ease'}}
				>
				...
			</div>
		</div>
	)
} 

폰트 파일 크기 줄이기

폰트 파일 크기를 줄이는 방법에는 두 가지가 있습니다.

폰트 포맷 변경하기

폰트 포맷은 운영 체제에서 사용하는 TTF 및 OTF 포맷입니다.

TTF포맷은 파일 크기가 매우 큽니다. 웹환경에서는 적절하지 않습니다.

그래서 나온 것이 WOFF입니다.(Web Open Font Format) → WOFF2

파일 크키 EOT > TTF/OTF > WOFF > WOFF2

WOFF,WOFF2 최신 브라우저에서만 사용가능하다.

폰트 변환 사이트

https://transfonter.org/

@font-face {
	font-family: BMYEONSUNG;
	src: url('./assets/fonts/BMYEONSUNG.woff2') format('woff2'),
			url('./assets/fonts/BMYEONSUNG.woff') format('woff'),
			url('./assets/fonts/BMYEONSUNG.tff') format('truetype'),
	font-display: block;
}

전체 폰트 불러오면 용량이 크니 필요한 폰트만 불러오는 방법도 있다.

서브셋 폰트 설정

@font-face {
	font-family: BMYEONSUNG;
	src: url('./assets/fonts/subset-BMYEONSUNG.woff2') format('woff2'),
			url('./assets/fonts/subset-BMYEONSUNG.woff') format('woff'),
			url('./assets/fonts/subset-BMYEONSUNG.tff') format('truetype'),
	font-display: block;
}

 

더 나아가서 폰트 파일 형태가 아닌 Data-URI 형태로 CSS 파일에 포함할 수도 있습니다.

Data-URI란 data 스킴이 접두어로 붙은 문자열 형태의 데이터인데, 쉽게 말해서 파일을 문자열 형태로 변환하여 문서(HTML, CSS, 자바스크립트 등)에 인라인으로 삽입하는 것입니다.

보통 App.css 파일이 로드된 후 폰트를 적용하기 위해 폰트 파일을 추가로 로드해야 하지만, Data-URI 형태로 만들어서 App.css 파일에 넣어 두면 별도의 네트워크 로드 없이 App.css 파일에서 폰트를 사용할 수 있습니다.

@font-face {
	font-family: BMYEONSUNG;
	src: url('data:font/woff;charset=utf-8;base64,asdfasdf.....')
	font-weight: normal;
	font-style: normal;
	font-display: swap;
}

캐시

  • 메모리 캐시 : 메모리에 저장하는 방식, 여기서 메모리는 RAM을 의미
  • 디스크 캐시 : 파일 형태로 디스크에 저장하는 방식
  • 어떤 캐시를 사용할지는 직접 제어할 수 없습니다. 브라우저가 사용 빈도나 파일 크기에 따라 특정 알고리즘에 의해 알아서 처리합니다.
  • 캐시가 적용된 리소스 헤더를 보면 Cache-Control 이라는 헤더가 들어 있습니다. 이 헤더는 서버에서 설정되며, 이를 통해 브라우저는 해당 리소스를 얼마나 캐시할지 판단합니다.

Cache-Control

  • 브라우저는 서버에서 이 헤더를 통해 캐시를 어떻게, 얼마나 적용해야 하는지 판단합니다.
  • no-cache: 캐시를 사용하기 전 서버에 검사 후 사용
    • 사용 전에 서버에 캐시된 리소스를 사용해도 되는지 한 번 체크하는 옵션
  • no-store: 캐시 사용 안 함
  • public: 모든 환경에서 캐시 사용 가능
  • private: 브라우저 환경에서만 캐시 사용, 외부 캐시 서버에서는 사용 불가
  • max-age: 캐시의 유효기간
  • public, private으로 설정하면 max-age에서 설정한 시간만큼은 서버에 사용 가능 여부를 묻지 않고 캐시된 리소스를 바로 사용합니다. 만약 유효시간이 지났다면 서버에 캐시된 리소스를 사용해도 되는지 다시 체크하고 유효 시간만큼 더 사용합니다. public과 private의 차이는 캐시 환경에 있습니다.
  • 웹 리소스는 브라우저뿐만 아니라 웹 서버와 브라우저 사이를 연결하는 중간 캐시 서버에서도 캐시 될 수 있습니다. 만약에 중간 서버에 캐시를 저장하고 싶지 않으면 private 옵션을 사용합니다.
  • max-age는 초 단위로 얼마나 오래 캐시를 사용할 것인지 설정합니다. 만약 max-age=60이면 60초동안 캐시를 사용한다는 의미
  • Cache-Control: max-age=60
    • 60초(1분) 동안 캐시를 사용합니다. private 옵션이 없으므로 기본 값인 public으로 설정되어 모든 환경에서 캐시를 합니다.
  • Cache-Control: private, max-age=600
    • 브라우저 환경에서만 600초(10분) 동안 캐시를 사용합니다.
  • Cache-Control: public, max-age=0
    • 모든 환경에서 0초동안 캐시를 합니다. 여기서 0초는 사실상 캐시가 바로 만료되는 상태이므로 매번 서버에 캐시를 사용해도 되는 지 확인을 합니다.
    • 즉, no-cache와 동일 설정이라고 볼 수 있습니다.

캐시 적용

const header = {
	setHeaders: (res, path) => {
		res.setHeader('Cache-Control', 'max-age=10')
	}
}
  • 새로고침 몇번 후 리소스 확인할때 size 에서 (memory cache)라고 나오면 캐시가 된 상태입니다.
    • status Code: 200 Ok
    • Reponse Headers에서 Cache-Control: max-age=10
  • 10초 뒤 즉 캐시가 만료되면 Size에서 260B 로 나온다.
    • status Code: 304 Not Modified
    • 캐시가 만료되서 서버에 캐시 사용해도 되는지 확인해보니 리소스 그대로니 사용해라
    • 260B 용량인 이유는 사용해도 되는지 네트워크 요청을 보냈기 때문

적절한 캐시 유효 시간

  • 리소스마다 캐시 유효 시간이 다르다.
    • HTML는 항상 no-cache입니다. 항상 최신 버전의 웹 서비스를 제공하기 위해서입니다. js,css는 파일명에 해시를 함께 가지고 있습니다.(main.bb8acca28.chunk.js), 즉 코드가 변경되면 해시도 변경되어 완전히 다른 파일이 되어 버립니다. 따라서 캐시를 아무리 오래 적용해도 HTML만 최신 상태라면 자바스크립트나 CSS 파일은 당연히 최신 리소스를 로드할 것입니다. 이미지도 마찬가지입니다.
    • html : no-cache
    • js,css,img: public, max-age=31536000

불필요한 CSS 제거

  • Coverage 패널 사용

PurgeCSS

  • PurgeCSS는 파일에 들어 있는 모든 키워드를 추출하여 해당 키워드를 이름으로 갖는 CSS 클래스만 보존하고 나머지 매칭되지 않는 클래스는 모두 지우는 방식으로 CSS를 최적화합니다.
<figure class="bg-slate-100 rounded-xl p-8">
  <div class="pt-6 space-y-4">PurgeCSS</div>
</figure>

  • figure, class, bg-slate-100, rounded-xl, p-8, div, pt-6, space-y-4 등이 추출된다.
  • 추출된 키워드와 Tailwind CSS에서 제공하는 유틸리티 클래스의 이름을 비교하여 일치하는 클래스만 남기는 방식입니다. 이렇게 하면 코드 내에서 사용하지 않는 클래스는 매칭되지 않으므로 CSS 파일에서 제거 될 것입니다.
npm install --save-dev purgecss

purgecss --css ./build/static/css/*.css --ouput ./build/static/css/ --content ./build/index.html ./build/static/js/*.js
  • --css 뒤에 있는 파일 선택
  • --output 으로는 동일한 위치를 지정함으로써 새로운 파일 생성하는 대신 기존 CSS 파일 덮어쓰드록 했습니다.
  • --content 로는 빌드된 HTML과 자바사크립트 파일 전부를 넣어주었습니다. 이렇게 하면 빌드된 HTML, 자바스크립트 파일의 텍스트 키워드를 모두 추출하여 빌드된 CSS 파일의 클래스와 비교하여 최적화하게 됩니다.
  • PurgeCSS는 devdepency로 설치되어 있으므로 위 스크립트를 그대로 실행하면 제대로 실행되지 않습니다.
  • npx, package.json의 scripts에 넣어줘야 합니다.
"scripts":{
	"purge": "purgecss --css ./build/static/css/*.css --ouput ./build/static/css/ --content ./build/index.html ./build/static/js/*.js"
}
  • 제대로 나오지 않는다
  • lg:m-8, lg:ml-32 같은 클래스에서 PurgeCSS가 텍스트 키워드를 추출할 때 콜론(:) 문자를 하나의 키워드로 인식하지 못하고 잘라 버렸기 떄문입니다.
  • lg:m-8이 lg 와 m-8 이라는 각각 다른 키워드로 인식

해결 방안

  • purgecss.config.js 생성
module.exports = {
	defaultExtractor: (content) => content.match(/[\\w\\:\\-]+/g) || []
}
  • defaultExtractor 옵션은 PurgeCSS가 키워드를 어떤 기준으로 추출하는 정의하는 옵션입니다.
  • 정규식을 사용

레이아웃 이동 피하기

  • CLS 최적화하기
  • 0부터 1까지의 값을 가지며 레이아웃 이동이 전혀 발생하지 않는 상태를 0, 그 반대를 1로 계산
  • 권장하는 점수는 0.1 이하

원인

  • 사이즈가 미리 정의되지 않은 이미지 요소
    • 브라우저는 이미지를 다운로드하기 전까지 이미지 사이즈가 어떤지 알 수 없으니 미리 해당 영역을 확보할 수 없습니다. 그렇기 때문에 이미지가 화면에 표시되기 전까지는 해당 영역의 높이(또는 너비)가 0입니다.
  • 사이즈가 미리 정의되지 않은 광고 요소
  • 동적으로 삽입된 콘텐츠
  • 웹 폰트(FOIT, FOUT)

해결

  • 해당 이미지 사이즈를 미리 예측할 수 있다면 또는 이미 알고 있다면 해당 사이즈만큼 공간을 확보해 놓는 것입니다.
  • 방법이 크게 2가지 있습니다.
// 전통적인 방법 padding 사용
<div class="wrapper>
	<img class="image" src"..." />
</div>

.wrapper {
	position: relative;
	width: 160px;
	padding-top: 56.25%; /* 16:9 비율 */
}

.image {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
}

// 최신 브라우저에서 사용 가능한 방법
.wrapper {
	width: 100%;
	aspect-ratio: 16 / 9;
}

.image {
	width: 100%;
	height: 100%;
}

 

728x90
반응형
LIST