프론트엔드/React

프론트엔드 성능 최적화하기

홍수성찬 2023. 8. 15. 20:59

프론트엔드에서 성능은 무엇일까?

웹 화면을 구현하고 서버로부터 데이터를 받아오는 과정에서 리소스들을 사용자들에게 빠르게 보여줄 수 있기 때문이다.

 

즉, "성능 == 속도"이다. 그렇다면, 성능이 왜 중요할까?

  1. 사용자 유지, 웹 사이트의 성능이 더욱 좋아질수록 사용자 참여도와 충성도 증가
  2. 사용자 경험, 웹 사이트 성능은 사용자 경험의 중요한 요소이며 느린 웹 사이트는 "사용자의 스트레스 증가"의 원인이고, 그로 잉ㄴ해 사용자가 웹 사이트를 "이탈할 가능성"이 상승
  3. 전환율 향상, 웹 사이트와 로딩 속도는 직접적으로 전환율에 영향
  4. 비용 절감, 성능이 좋지 않은 웹 사이트는 사용자에게 실질적인 비용을 초래하며, 데이터 요금제를 사용하는 모바일 사용자는 "페이지 크기가 커질수록 추가 비용"이 발생

 

실제로 비즈니스 관점에서 대기 시간을 줄였을 때 즉, 최적화를 했을 때 가입 수는 늘고 이탈율은 낮추는 현상을 자주 볼 수 있었다.

 

또한, "SEO(검색 엔진 최적화)" 관점에서 Core Web Vitals의 Loading, Interactivity, Visual Stability 최적화가 진행된 경우에 페이지 경험을 위한 검색 신호가 더 좋아질 수 있다.

 

Core Web Vitals

  • LCP (Largest Contentful Paint, Loading)
    • 로딩 성능 측정
    • 페이지가 처음으로 로드를 시작한 시점을 기준으로 뷰포트 내에 있는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간 측정
    • <img>, <svg> 내 <image>, <video>, url() 함수를 통해 로드된 배경 이미지, 텍스트 노드 또는 기타 인라인 수준 텍스트 하위 요소를 포함하는 블록 수준
    • 우수한 사용자 경험을 제공하기 위해서 페이지가 처음으로 로딩된 후 "2.5 초" 이내에 LCP 발생 필요
  • FID (First Input Delay, Interactivity) → INP (Interaction to Next Paint) 2024년 3월 대체
    • 사용자의 상호 작용 측정
    • 사용자가 페이지와 처음 상호 작용할 때부터 해당 상호 작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시간 측정
    • 우수한 사용자 경험을 제공하기 위해 페이지의 FID가 "100ms" 이하 필요
  • CLS (Cumulative Layout Shift, Visual Stability)
    • 누적 레이아웃 이동
    • 사용자가 예상치 못한 레이아웃 이동에 대한 경험을 수량화하여 측정
    • 시각적 안정성 측정
    • 리소스가 비동기식으로 로드되거나 DOM 요소가 기존 콘텐츠 위의 페이지에 동적으로 추가되기 때문에 발생
    • 우수한 사용자 경험을 제공하기 위해 페이지에서 "0.1"이하의 CLS 유지 필요
    • 예. 버튼 이벤트 클릭을 실행하며 일어나는 현상 측정
  • INP (Interaction to Next Paint)
    • 2024년 3월, FID와 대체 예정인 지표
    • 첫 번째 상호작용만 측정하는 FID와 달리 모든 페이지 상호작용 고려

 

전통적인 성능 지표

현상

  • 서버 응답이 성공했는가?
  • 내비게이션이 성공적으로 시작됐는가?

 

유용성

  • 사용자가 참여할 수 있는 충분한 콘텐츠가 렌더링됐는가?

 

사용성

  • 사용자가 웹 페이지와 상호작용할 수 있는가?
  • 아직 로딩중인가?

 

마음에 드는 것

  • 상호작용이 지체없이 부드럽고 자연스러운가?

 

결론은 "성능이 좋다", "사이트가 빠르다"라는 말은 하드웨어나 네트워크 성능, 점진적 렌더링, 다양한 사용자와 관련이 있다. 여기서 속도는 "상대적인 개념"이기 때문에 이는 개인마다 다르다는 것을 알아두어야 한다.

 

사용자 정의 지표

사용자 정의 지표가 제공하는 특징들을 이해한 다음 사용자 경험에 중요한 부분들을 설정하는 것이 중요하다.

콘텐츠가 출력되기 전에 로딩 화면을 나타낼 건지, 각 요소의 블록을 나타낼 것인지 설정하는 것이 중요하다는 것이다.

 

실험실 도구

일관되고 통제된 환경에서 페이지 로드를 시뮬레이션하는 도구이다.

  • Chrome DevTools
  • Lighthouse

 

필드 도구

실제 사용자가 실제로 페이지를 로드하고 페이지와 상호 작용하는 도구이다.

  • Chrome UX Report
  • web-vitals Library
  • Chrome Web Vital Extension

 

Lighthouse

웹 페이지 품질을 검사하기 위한 오프 소스 도구이다.

DevTools의 Lighthouse 패널에서 사용할 수 있다.

 

이 도구는 실험실 환경에서 성능 및 접근성 등의 사용자 경험 품질을 측정한다.

사이트 성능에 영향을 주는 항목과 각 항목별로 설명과 함께 개선 방안을 제공한다.

 

그러나, 페이지와 사용자의 상호작용을 확인할 수 없는 실험실 도구이므로 "FID" 대신 확인할 수 있는 지표인 "TBT"를 제공한다.

 

"TBT(총 차단 시간)"은 메인 스레드가 입력 응답을 막을 만큼 오래 차단되었을 때의 시간을 측정하는 것을 말한다.

 

Performance Insights

웹 사이트 성능을 측정할 수 있는 DevTools 패널이며, 기존의 Performance 패널의 불편한 점들을 개선하기 위해 등장한 실험 단계의 기능이다.

 

더 간소화된 UI, 사용 사례에 따른 분석 지원, 브라우저 작동 방식에 대한 전문 지식이 없어도 사용할 수 있도록 지원한다.

 

기타 Chrome DevTools

  • Coverage
  • 개발자모드(F12)의 Network

 

Chrome UX Report (CrUX)

인기 사이트의 실제 사용자 경험 데이터를 제공하는 공개 데이터 세트이다.

모든 사용자 중심의 Core Web Vitals 메트릭이 데이터 세트에 표시된다.

 

유의미한 데이터를 제공받기 위해 공개적으로 검색이 가능하고, 충분한 사용자가 있어야 한다.

 

  • CrUX on BigQuery
  • CrUX Dashboard (Looker Studio)
  • CrUX API
  • CrUX History API
  • PageSpeed Insights
  • PageSpeed Insights API
  • Google Search Console

 

잠깐, 코드로 설명할 수 있는 "web-vitals Library"는 어떻게 사용할 수 있을까?

import {onCLS, onFID, onLCP} from 'web-vitals'

function sendToAnalytics(metric) {
	const body = JSON.stringify(metric)
    
    (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) || fetch('/analytics', {body, method: 'POST', keepalive: true})
}

onCLS(sendToAnalytics)
onFID(sendToAnalytics)
onLCP(sendToAnalytics)

 

프론트엔드 성능 최적화, 개선

LCP 개선

  • 서버 응답 시간 개선
  • 리소스 요청 최적화
  • CSS 축소, 지연, 즉시 처리

 

FID 개선

  • 사용하지 않는 자바스크립트 분할
  • 자바스크립트 실행 시간 단축
  • 웹 작업자 사용

 

CLS 개선

  • 이미지 크기 설정
  • 광고를 위한 고정 공간 확보
  • 동적 콘텐츠 대응
  • FOUT/FOLT를 유발하는 웹 글꼴 대응

* Bold - 적은 노력으로 큰 변화를 가져올 수 있는 방법

 

서버 응답 시간 개선

가까운 CDN으로 라우팅해야 한다.

예. CDN 서버를 기존 워싱턴에서 서울로 변경

만약, 이렇게 할 경우 응답 시간을 크게 줄이면서 속도를 많이 향상시킬 수 있을 것이다.

 

이미지 최적화

이미지 사이즈 조정 및 압축해야 한다.

예. 이미지 원본 3,024 * 4,032 4.3MB → 이미지 사이즈 조정 및 압축 300 * 450 30.6KB

이럴 경우, 이미지 로드 시간을 확연히 줄일 수 있고, 속도를 크게 향상시킬 수 있다.

이미지 사이즈를 조정하거나 압축시킬 수 있는 라이브러리 혹은 플러그인은 "Imagemagick"과 "Imagemin"이 있다.

  • Imagemagick, 이미지 사이즈 조정 라이브러리로 이미지를 새로 만들거나 고칠 수 있는 도구. CLI로 간단하게 이미지 사이즈 조정 가능
  • Imagemin, 이미지 압축 플러그인으로 다양한 이미지 형식을 지원하는 이미지 압축 도구로 CLI, NPM 패키지, Webpack 플러그인에서 사용 가능

 

반응형 이미지를 활용하거나, 이미지 지연 로딩을 활용할 수 있다.

브라우저 크기에 따라 그에 맞는 작은 사이즈의 이미지를 각각 활용하는 것이다.

 

반응형 이미지

환경에 맞게 적절한 사이즈의 이미지를 제공하는 방법이다.

 

<img
	srcSet='/assets/sam-300w.png 300w, /assets/sam-600w.png 600w, /assets/sam-900w.png 900w'
    sizes='(max-width: 300px) 300px, (max-width: 600px) 600px, (max-width: 900px) 900px'
    src='assets/sam-900w.png'
    alt='이미지 최적화'
/>
  • srcset, 브라우저에 제시할 이미지 목록과 크기 정의
  • sizes, 화면 크기 조건 정의

 

브라우저 동작

  1. 기기 너비 확인
  2. sizes 목록에서 조건 확인
  3. 해당 조건에서 이미지 크기 확인
  4. 크기에 근접한 srcset의 이미지 요청

 

물론, 이미지 사이즈에 따라 크기가 다르기 때문에 최적화에 도움이 될 것이다.

 

사용하지 않는 자바스크립트 분할

// App 컴포넌트
import Home from './pages/Home'
import About from './pages/About'

const App = () => {
	return (
    	<Routes>
        	<Route path='/' element={<Home />} />
            <Route path='/about' element={<About />} />
        </Routes>
    )
}

// Home 컴포넌트
cosnt Home = () => {
	return <div>Home</div>
}

export default Home

// About 컴포넌트
import { FaBeer } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import styled from 'styled-components'

const About = () => {
	return (
    	<div>
        	<Link to='/'>
            	<FaBeer />
                <StyledAbout>About</StyledAbout>
            </Link>
        </div>
    )
}

export default About

const StyledAbout = styled.div`
	background-color: red
`

 

위 코드로 구현을 하면, 모든 페이지를 로드해 자바스크립트 번들 사이즈가 커진다.

그래서, 페이지 요청이 들어갔을 때, 로드가 되도록 즉, 요청된 컴포넌트 코드의 로딩을 지연시켜서 최초 번들 사이즈를 줄여야 한다.

 

// App 컴포넌트
import {lazy} from 'react'
import {Route, Routes} from 'react-router-dom'

import Home from './pages/Home'
const About = lazy(() => import('./pages/About'))

const App = () => {
	return (
    	<Routes>
        	<Route path='/' element={<Home />} />
            <Route path='/about' element={<About />} />
        </Routes>
    )
}

export default App

"React.lazy"를 사용하여 처음 렌더링이 될 때까지 컴포넌트 코드의 로딩을 지연시킨다. (= Dynamic imports)

 

"lazy"를 사용해 번들을 분할시키고 사용자가 필요한 자바스크립트만을 제공하도록 구현한다.

 

이미지 크기 설정

<img
	src='/assets/img.jpeg'
    alt='이미지'
    style={{ display: 'block', width: '300px', height: 'auto' }}
/>

위 코드처럼, 이미지 크기를 스타일로 지정하는 것이 아니라, 이미지 요소에 있는 "width, height" 속성을 추가하는 것이 더욱 나은 방법이다. 이렇게 구현함으로써 이미지를 가져오기 전에 공간을 확보할 수 있다.

 

<img
	src='/assets/img.jpeg'
    alt='이미지'
    width={300}
    height={400}
    style={{ display: 'block' }}
/>

이를 통해, "CLS" 개선이 가능하다.

추가적으로 이미지 압축을 실행하면, 더욱 좋은 최적화가 진행되어 좋은 퍼포먼스 점수를 얻을 수 있다.


참조