웹 성능 최적화(개선)은 웹페이지를 효율적으로 동작하게 하는 작업입니다.
성능을 최적화함으로써 사용자에게 좋은 사용자 경험을 제공할 수 있기 때문에 이는 결국 비즈니스적인 성공과 직결되는 문제이기도 합니다.
예를 들어, 사용자가 웹 사이트에 접속했을 때 하얀 화면(빈 화면)을 노출시키거나 웹 사이트 로딩 및 렌더링에 지연이 발생하면 사용자는 자연스레 이탈을 하게 됩니다. 그리고 동시에 해당 웹 서비스는 방문 사용자에 대한 비즈니스 기회를 잃어버리게 됩니다.
웹 페이지 로딩 시간과 사용자 이탈률은 밀접한 관계를 갖고 있습니다.
아래 자료에 따르면 페이지 로딩 시간이 1초에서 3초로 증가하면 페이지 이탈률이 32%로 증가한다고 하고, 10초가 되면 123%나 된다고 합니다.
즉, 웹 사이트가 사용자에게 빨리 로딩되면 좋은 사용자 경험(UX, User Experince)을 제공할 수 있고 초반 이탈률도 감소시킬 수 있습니다.
UX를 위해 사용자의 뷰(view), 즉 사용자 시선에 따라 콘텐츠가 어떻게 로드되어 보여지는지는 것도 신경을 써주는 것이 좋습니다.
일반적으로 사용자의 시선은 위에서 아래로 이동합니다.
좌우의 경우에는 나라마다 차이가 존재합니다.
한국과 같은 LTR(Left to Right) 국가들을 기준으로 개발을 할 때에는 콘텐츠의 로드 순서가 위부터, 왼쪽부터 되도록 신경을 쓴다면 자연스러운 사용자 경험을 제공할 수 있습니다.
콘텐츠의 로딩 순서가 웹 성능 점수에 영향을 미치지 않지만, 사용성에 영향을 끼치므로 웹 성능 작업을 하면서 콘텐츠 로딩 되는 순서를 되짚어 보고 개선하는 것이 좋을 것 같습니다.
예를 들어, 웹 사이트 표출 시, 사이트 윗부분인 Heder 부분이 늦게 뜨고 아래서부터 페이지가 로딩 된다면 어색한 사용자 경험을 제공하게 됩니다. 🙃
그렇다면 프론트엔드 단에서 할 수 있는 최적화에는 어떤 것들이 있을까요?
크게 로딩 성능과 렌더링 성능으로 나눌 수 있을 것 같습니다.
얼마나 빠르게 리소스를 로드하는가
웹 사이트의 로딩 성능은 서버에서 웹 페이지에 필요한 HTML, CSS, Javascript, 미디어 소스(Image, Video) 등의 리소스를 다운로드할 때의 성능을 말합니다.
얼마나 빠르게 화면을 렌더링하고 있는가
웹 사이트의 렌더링 성능은 페이지 화면에 주요 리소스가 페이지에 그려질 때의 성능을 말합니다.
위 이미지는 Google이 만든 필수적인 웹 성능 지표인 Core Web Vital입니다.
LCP, CLS는 개발자 도구의 Lighthouse에서 정량적인 수치로 확인할 수 있습니다.
분석은 개발자 도구의 탭을 활용해 할 수 있습니다.
자주 사용되는 탭들은 총 네 가지, lighthouse(별도 설치 필요), profiler, performances, network 입니다.
현재 만들고 있는 개인 포트폴리오 웹 페이지로 테스트해보겠습니다.
개발자 도구에서 lighthouse 탭에 들어가 페이지 로드 분석하기 버튼을 누르면
분석이 시작됩니다.
로드되는 순서대로 검사를 진행해주며 프로젝트의 크기에 따라 로딩 시간이 다소 걸릴 수도 있습니다.
분석이 완료되면 최상단에서 현재 웹 페이지를 Performance
, Accessibility
, Best Practices
, SEO
, PWA
총 다섯 가지 기준에 따라 분석 점수를 확인할 수 있습니다. 스크롤을 좀 더 내리면 세세한 항목도 확인이 가능합니다.
여기에 분석된 내용을 토대로 최적화를 진행해보려고 했는데 점수가 상당히 높게 나왔네요.😅
Performance의 점수는 현재 페이지의 성능을 측정한 점수이며, 이는 Metrics 지표의 세부 항목을 기준으로 측정됩니다.
1. First Contentful Paint (첫 번째 콘텐츠 렌더링)
2. Time to Interactive (상호 작용 가능 시간)
3. Speed Index (속도 지수)
4. Total Blocking Time (총 차단 시간)
5. Largest Contentful Paint (가장 큰 콘텐츠 렌더링)
6. Commulative Layout Shift (누적 레이아웃 변경)
Oppertunities
와 Dignostics
는 현재 웹페이지의 문제점과 성능 최적화를 위한 가이드를 제시해주는 부분입니다.
Opportunities
는 로딩 성능과 관련된 내용으로 어떻게 리소스를 더 빠르게 로딩할 수 있는지의 관점에서 개선 포인트를, Dignostics
는 렌더링 성능과 관련된 내용으로 그 개선점을 진단해줍니다.
tree map 버튼을 누르면 파일별 차지하는 크기도 확인할 수 있습니다.
Profiler는 성능 데이터를 기록하고 측정을 해줍니다.
그리고 프로파일링은 개발 환경에서만 확인이 가능합니다. 즉, 배포가 된 사이트에서는 확인이 불가합니다. 우측 gif와 같이 프로파일링을 시작할 수 있습니다.
Flamegraph 차트를 보면 App 컴포넌트는 렌더링하는데 어느 정도의 시간이 걸렸는지 알 수 있습니다.
Ranked 차트에서는 파일의 어떤 컴포넌트의 렌더링 시간이 오래 걸린 순서를 확인할 수 있습니다.
Performance 패널에서는 Timeline을 기준으로 페이지가 로드되면서 실행되는 작업들에 관한 정보를 그래프와 화면들의 스냅샷으로 확인할 수 있습니다.
Screenshots 옵션을 활성화 한 경우 확인 가능하며, Timeline에 따른 렌더링 과정을 스냅샷을 통해 확인할 수 있습니다.
DCL, FP, FCP, LCP, L 등의 순서를 확인할 수 있습니다.
DCL (DOMContentLoaded event)
HTML과 CSS parsing이 완료되는 시점
렌더 트리를 구성할 준비가 된 (DOM 및 CSSOM 구성이 끝난) 상황을 의미
FP (First Paint)
화면에 무언가 처음으로 그려지기 시작하는 순간
FCP (First Contentful Paint)
화면에 텍스트나 이미지가 출력되기 시작하는 순간
FMP (First Meaningful Paint) ⭐️
사용자에게 의미있는 콘텐츠가 그려지기 시작하는 첫 순간
콘텐츠 노출에 필요한 리소스(css, JavaScript file) 로드가 시작되고 스타일이 적용된 시점
L (onload event)
HTML 상에 필요한 모든 리소스가 로드된 시점
이 중 FMP의 시점은 사용자에게 필요한 컨텐츠가 노출되는 시점, 즉 웹 사이트에 대한 사용자의 첫 인상이 결정되는 순간이기 때문에 주의 깊게 살펴볼 필요가 있습니다.
FMP 시점을 앞당기는 것을 사용자 기준의 성능 최적화의 지표로 삼을 수도 있습니다. 이 과정에서 어떤 컨텐츠가 가장 먼저 노출되어야 하는가에 대한 논의가 필요하며 개발 과정에 반영되어야 합니다.
Timeline에 따른 이벤트와 그에 따른 부작업을 확인할 수 있습니다.
각각의 막대는 이벤트를 나타내며, 폭이 넓을 수록 오래 걸린 이벤트입니다. 각 이벤트 아래쪽의 이벤트들은 상단의 이벤트로부터 파생된 이벤트입니다.
마우스 휠로 확대와 축소가 가능하므로 보고 싶은 부분을 확대해 면밀히 살펴볼 수 있습니다.
Network는 Performance 패널과 함께 레코딩되며, 웹 페이지가 로딩되는 동안 요청된 리소스 정보들을 확인할 수 있습니다.
이 때 리소스 목록은 시간순으로 정렬되며, 아래와 같이 각 리소스의 서버 요청 대기 시간을 확인할 수 있습니다.
Queuing
대기열에 쌓아둔 시간
Stalled
요청을 보내기 전의 대기 시간 (서버와 커넥션을 맺기까지의 시간)
Waiting (TTFB)
초기 응답(Time To First Byte)을 받기까지 소비한 시간(서버 왕복 시간)
Content Download
리소스 다운에 소요된 시간
저는 lighthouse에서 진단받은 내용을 토대로 몇 가지 고쳐보도록 하겠습니다.
아까 lighthouse에서 performance 점수가 90/100 이었는데 그 중 세 가지 항목에서 빨간색 세모 표시(🔺)를 받았습니다.
1️⃣ eliminate render-blocking resources | 렌더링 차단 리소스 제거하기
렌더링 차단 리소스란, 페이지의 렌더링을 차단하는 리소스를 의미합니다. 이러한 리소스를 최적화하면 페이지의 로딩 속도를 개선할 수 있습니다.
렌더링 차단 리소스를 제거하기 위해서는 우선 렌더링 차단 URL을 파악해야 합니다. Lighthouse나 Page Speed Index와 같은 사이트 퍼포먼스 테스트 진단 결과를 확인하면 FCP(First Contentful Paint)를 차단하는 모든 URL이 나열됩니다.
렌더링 차단 URL을 파악한 후, 다음과 같은 방법으로 렌더링 차단 리소스를 제거할 수 있습니다.
중요한 리소스를 인라인으로 전달하기
중요한 CSS나 JS 파일을 HTML 문서에 직접 포함시켜 인라인으로 전달할 수 있습니다. 이를 통해 브라우저가 별도의 리소스를 요청하지 않아도 되므로 로딩 속도를 개선할 수 있습니다. 하지만 이 방법은 HTML 문서의 크기를 증가시키므로, 불필요한 리소스는 인라인으로 전달하지 않도록 주의해야 합니다.
중요하지 않은 리소스를 지연하기
중요하지 않은 CSS나 JS 파일은 페이지 로딩이 완료된 후 지연시켜 로딩 속도를 개선할 수 있습니다. 이를 위해 defer나 async 속성을 사용하거나, JavaScript를 사용하여 리소스를 동적으로 로딩하도록 구현할 수 있습니다.
사용되지 않는 리소스 제거하기
사용되지 않는 CSS나 JS 파일은 제거하여 로딩 속도를 개선합니다. 이를 위해 불필요한 코드를 제거하거나, 사용되지 않는 라이브러리를 제거할 수 있습니다.
❗️주의사항❗️
1️⃣ 인라인으로 전달하는 리소스가 너무 많으면 안 돼요!
HTML 문서의 크기가 커져 로딩 속도가 느려질 수 있습니다.
2️⃣ 중요하지 않은 리소스를 지연시키면 안 돼요!
페이지의 로딩이 완료되기 전까지 해당 리소스를 사용할 수 없으므로, 페이지의 기능이 동작하지 않거나 깨질 수 있습니다.
3️⃣ 사용되지 않는 리소스를 제거할 때 백업을 남겨주세요!
코드의 유지보수나 확장성을 고려하여, 필요할 때 다시 추가할 수 있도록 백업을 남겨두는 것이 좋습니다.
현재 제 페이지에서 렌더링을 차단시키는 리소스들은 app/layout.css
와 app/page.css
, 총 두 개가 있습니다.
app/layout.css
에는 nextJS로 프로젝트 폴더와 함께 자동 생성된 내용으로 다크모드 등 사용하지 않는 코드가 많이 적혀 있습니다.
현재 제 포트폴리오 디자인과 상관 없는 코드들을 전부 삭제하니 transfer size가 조금 줄어들었습니다. (performance도 5점 올랐습니다. 😃)
다음 2, 3번 항목은 메인 페이지에 들어가는 이미지의 사이즈와 관련된 항목입니다. 가져오는 이미지의 용량이 현재 보여지는 화면의 용량보다 크기 때문에 과도하게 많은 용량을 가져온다는 의미로 이미지의 사이즈를 줄이라고 권고한다고 보면 됩니다. 🙂
2️⃣ Largest Contentful Paint image was lazily loaded
이미지 요소를 지연 로드하는 경우 브라우저에 이 이미지의 우선 순위를 해제하도록 명시적으로 지시합니다.
지연 로드된 이미지는 훨씬 나중에 지연되지 않은(non-lazy) 이미지를 다운로드하기 위해 대기열에 추가됩니다. 이로 인해 이미지가 나중에 렌더링되고 콘텐츠가 포함된 최대 페인트 측정 항목이 지연됩니다.
이는 지연 로드된 이미지가 아닌 다른 모든 이미지가 지연 로드될 이미지보다 먼저 다운로드되도록 예약된다는 의미입니다. 따라서 지연 로드 이미지가 LCP 요소인 경우 LCP가 지연될 수 있습니다.
Lazysizes.js
와 같은 자바스크립트 기반 지연 로딩 라이브러리를 사용하면 상황이 더욱 악화됩니다. 이제 브라우저는 LCP 이미지 요소를 지연시킬 뿐만 아니라 자바스크립트가 다운로드되어 실행될 때까지 기다려야 합니다.
LCP 이미지에서 loading="lazy"
속성을 제거하거나 LCP 이미지에 대한 지연 로딩을 우회하도록 플러그인의 필터를 업데이트하여 lighthouse의 경고를 없앨 수 있습니다.
기본 지연 로딩을 사용하는 경우
JavaScript 기반 지연 로딩을 사용하는 경우
이미지 구성 요소를 사용하는 경우 (ex. next/image)
저는 NextJS에서 Image 태그를 사용했기 때문에 loading="eager"
설정을 추가했습니다. strategy
의 경우 타입과 관련된 에러가 발생해 loading로 설정했습니다.
// page.tsx의 return 문
<AnimatePresence mode="wait">
<main className={style.main}>
<div className={style.ImageContainer}>
<motion.div whileHover={{ scale: 1.1 }}>
<Image
onClick={() => router.push("/Amy")}
src={profile}
className={style.profile}
loading="eager" ✅
alt="profile image"
/>
</motion.div>
</div>
<motion.div exit={{ opacity: 0 }} className= {style.textbox}>
<p>JUYEON OH</p>
<p>FE DEVELOPER</p>
</motion.div>
</main>
</AnimatePresence>
performance 점수가 오르진 않았지만 경고가 사라진 것을 확인할 수 있었습니다.
이미지의 용량은 아래와 같은 방법으로 줄일 수 있습니다.
| 이미지 최적화
이미지 최적화 도구를 사용하여 이미지를 압축하고 최적화합니다. 이를 통해 이미지의 파일 크기를 줄이고 페이지 로드 속도를 개선할 수 있습니다. 예를 들어, ImageMagick, TinyPNG, JPEG Optimizer 등의 도구를 사용할 수 있습니다.
| image cdn
이미지를 사용자에게 보내기 전에 jpg의 포맷을 바꾸거나, 용량을 줄여서 사용자에게 전송할 수 있는 방법입니다.
예를 들어, 1200짜리를 120으로 줄여서 사용자에게 전달하고자 할 때 http://cdn.image.com?src=img.src&width=200&height=100
와 같은 방법으로 표시하는 겁니다.
단, 이미지를 가져오는 매체 (unsplash, aws s3)에서 image cdn을 자체 제공해주면 굳이 사제 이미지 cdn을 사용 안해도 됩니다.
| Responsively size images
반응형 웹 디자인을 사용하여 이미지 크기를 디바이스 및 화면 크기에 맞게 조절합니다. Next.js에서는 <Image>
컴포넌트를 사용하여 이미지를 반응형으로 처리할 수 있습니다. 이미지의 너비와 높이를 잘 조절하여 필요 이상으로 큰 이미지를 피할 수 있습니다.
나아가 Next.js는 자동으로 다양한 이미지 포맷을 지원하며, srcset 속성을 생성하여 브라우저가 지원하는 최적의 이미지를 선택하도록 합니다.
srcset 속성은 아래와 같이 사용할 수 있습니다.
import Image from 'next/image';
const MyImage = () => {
return (
<Image
src="/images/example.jpg"
alt="Example Image"
width={500}
height={300}
srcSet={{
'/images/example-400.jpg': 400,
'/images/example-800.jpg': 800,
'/images/example-1200.jpg': 1200,
}}
/>
);
};
export default MyImage;
저는 이미지가 크기 별로 존재하는 것도 아니고 외부 사이트에서 가져오는 이미지도 아니기 때문에 Next.js에서는 <Image>
컴포넌트의 속성을 활용해 이미지 최적화를 진행했습니다.
현재 제 페이지에 렌더링된 이미지의 크기는 위의 1/2 이지만 화질이 덩달아 낮아지는 것을 확인해 2배 크기로 불러오도록 설정했습니다.
<Image src={profile} loading="eager" width={400} height={482} />
점수에는 변화가 없었지만 마찬가지로 경고문이 사라졌습니다.
이미지를 최대한 빠르게 로드하기 위해선 먼저 이미지 크기를 최적화하고, 모바일에서도 빠르게 로드되도록 이미지를 압축해야 합니다.
이미지 크기의 최적화는 이미지 해상도를 줄이는 것 뿐만 아니라 이미지의 포맷도 고려해야 합니다.
또한, 이미지를 미리 로드하는 것도 페이지의 로드 시간을 단축할 수 있는 한 방법입니다. 이미지를 미리 로드하게 되면 브라우저가 이미지를 로드하는 시간동안 다른 요소들의 로드와 렌더링을 진행할 수 있게 되기 때문입니다.
| rel="preload"
<Link rel="preload" />
Link 태그의 속성을 사용하여 이미지를 미리 로드할 수 있습니다.
이 방법은 브라우저가 페이지를 로드하기 전에 이미지를 미리 로드하도록 하는 설정입니다.
| IntersectionObserver
IntersectionObserver
를 사용해 이미지가 브라우저의 뷰포트 안에 들어왔을 때 로드할 수 있도록 합니다. 이를 활용할 경우 불필요한 이미지 로드를 방지할 수 있습니다.
| lazy-loading
<img loading="lazy" />
이미지 태그의 속성을 사용해 위와 동일한 효과를 볼 수 있습니다. 하지만 lazy-loading의 경우 모든 브라우저에서 지원되는 기능이 아니기 때문에 둘 중에서는 Intersection Observer의 사용을 권장합니다.
위 경고(페이지가 뒤로/앞으로 캐시 복원을 방해했습니다)는 사용자가 이전 또는 다음 페이지로 이동할 때 브라우저 캐시에서 페이지를 복원하는 것을 방해하는 페이지의 정책이 있음을 나타냅니다.
해당 경고는 아래와 같은 부분을 검토함으로써 해결할 수 있습니다 :
1️⃣ Cache-Control 헤더 검토
- 페이지의 HTTP 응답 헤더에서 Cache-Control 헤더를 확인
- "no-store" 또는 "no-cache"와 같은 값이 있는지 확인
- 브라우저 캐시를 비활성화/새로 고침 → 항상 새로운 콘텐츠를 요청하도록 강제
- 필요에 따라 적절한 캐싱 헤더를 설정
2️⃣ Service Worker 확인
- 페이지에서 사용 중인 Service Worker가 캐시 동작을 제어하는지 확인
- 페이지의 캐싱 및 오프라인 지원 담당 가능 → 캐시 관련 동작 확인
3️⃣ History API 사용 검토
- 페이지가 History API를 사용하여 페이지 이동을 관리하는 경우, 페이지 전환에 대한 캐싱 동작을 방해 가능성 존재
- story API를 사용하여 페이지 전환을 구현하는 경우, 캐싱 동작을 적절히 처리하도록 수정
4️⃣ 브라우저 지원 정책 검토
- 페이지 전환에 대한 캐싱 동작을 제어하는 정책 존재
- 브라우저의 지원 문서 → 페이지 전환에 대한 캐싱 동작을 이해하고, 필요한 조치 취하기
5️⃣ 레거시 코드 확인
- 페이지에 오래된 레거시 코드가 있는 경우, 페이지 캐시 동작을 방해 가능성 존재
- 레거시 코드를 수정/업데이트 → 최신 캐시 관련 규약을 준수
저는 총 네 개의 항목에서 걸려 이런 경고를 받았습니다.
하지만 네 가지 모두 localhost:3000 일 때 받는 경고이기 때문에 확인만 하고 넘어갈 예정입니다. (배포가 된 사이트에서는 해당 경고가 없습니다😃)
WebSocket 사용
WebSocket을 사용하는 페이지가 뒤로/앞으로 캐시에 들어갈 수 없다는 것을 의미
WebSocket은 기본적으로 브라우저의 기존 캐시 기능과 호환되지 않을 수 있습니다. 이 경우, WebSocket을 사용하는 대신에 다른 통신 방식을 고려해보거나, WebSocket이 필요한지 다시 검토할 필요가 있습니다.
cache-control:no-store
페이지의 주 리소스가 cache-control:no-store로 설정되어 있다는 것을 의미
이 설정은 브라우저가 해당 리소스를 캐시하지 않도록 합니다. 따라서 이 설정을 변경하여 브라우저가 페이지를 캐시할 수 있도록 해야 합니다.
JsNetworkRequestReceivedCacheControlNoStoreResource
JavaScript 네트워크 요청에서 cache-control:no-store 리소스를 수신했다는 것을 나타냅니다. 이 경우, 해당 리소스의 cache-control 값을 수정하여 브라우저가 해당 리소스를 캐시할 수 있도록 변경해야 합니다.
WebSocketSticky
이 항목은 WebSocketSticky가 존재한다는 것을 의미합니다. WebSocketSticky는 브라우저 캐시와 관련된 문제를 발생시킬 수 있는 추가적인 설정이므로, 필요한 경우 해당 설정을 검토하고 변경해야 합니다.
이 경고는 웹 페이지의 성능을 저하시킬 수 있는 중요한 문제 중 하나로 웹 페이지에서 불필요한 JavaScript 코드가 발견되었을 때 나타납니다. 불필요한 JavaScript 코드는 페이지의 다운로드 시간을 증가시키고 실행 시간을 늘릴 수 있어, 사용자 경험을 저하시키고 페이지의 로드 속도를 느리게 할 수 있습니다.
Detect unused JavaScript
사용되지 않는 JS 코드는 Coverage
탭에서 리스트를 확인할 수 있습니다.
그리고 각 코드/파일마다 아래 세 가지 방법 중 적합한 것으로 처리하면 경고를 해결할 수 있습니다.
1️⃣ Code Splitting
2️⃣ Unused Code Elimination
3️⃣ Unused Imported Code
Coverage
탭에서 항목 클릭 시 어떤 코드가 문제인지 정확히 짚어줍니다.
혹시 몰라 사용하게 될 수 있어 남겨두었던 미사용 파일(애니메이션 관련)들을 전부 제거했습니다.
최종적으로 현재 제작 중인 포트폴리오 사이트의 성능은 아래와 같습니다. 🥳
100점이어도 진단 시 경고 받은 것들이 있어서 아래 부분들은
남은 포트폴리오 진행하면서 함께 해결 예정입니다.🙂
References
웹 프론트엔드 개발자가 웹 성능 최적화를 해야 하는 이유
React Profiler를 사용하여 성능 측정하기
React 리렌더링과 성능
React 성능 최적화
웹 성능 분석 및 최적화 기법 (with Chrome Developer Tools)
렌더링을 방해하는 리소스 제거하기
Largest Contentful Paint image was lazily loaded
Largest Contentful Paint image was lazily loaded solution
properly size images
콘텐츠가 포함된 최대 paint image 미리 로드
reduce unused JS