Lighthouse의 측정 지표들로 성능을 분석하고, 메인 페이지의 성능을 직접 개선하는 과정을 담아보았다!
빌드도구 : Vite 4.2
프레임워크 : Vue3
시작하기 앞서, 3가지를 먼저 알고 가자!
# 알고가기
1. 측정시에는 시크릿모드에서 측정하는 것을 권장한다.
크롬 브라우저 플러그인, extension이 있는 경우 성능을 측정하는데 지연이 될 수 있다.
2. Lighthouse의 측정 결과는 시시각각 다를 수 있다.
이런저런 시도를 할때마다 결과에 편차가 있어서, 내가 시도한 과정들이 정말 유효한지 헷갈렸었다.
아래 이미지를 보면, 실제 같은 날, 같은 코드로 돌린 결과에 이렇게 차이가 있었다. 시시각각 결과에 차이가 있다는 것을 모르고 테스트한다면, 일시적인 결과나 우연에 의존한 결정을 내릴수도 있으니 알고 있으면 좋을 것 같다!
사이트마다 이유는 다르겠지만, 네트워크 측면에서 인터넷 트래픽에 따른 라우팅의 변경이 있을 수도 있고,
성능 측정하는 하드웨어 및 서버 상황에 따라 측정 결과가 다르게 나올 수 있다고 한다.
심지어는 로컬이나 서버 측정 경우에도 바이러스 백신 SW가 동작함에 따라 성능 측정에 영향을 줄 수 있어,
성능 점수보다는 측정 항목 및 개선점에 중점을 두는게 좋다고 한다.
측정 결과는 매번 다를 수 있기에, 10회 정도 측정하고 평균을 내는 것도 좋은 방법이다.
실제 소프트웨어 성능 테스트를 받을 때 이렇게 여러번 한 결과에 대한 평균으로 성능을 측정하기도 한다.
3. 성능 측정시에는 local이 아닌 production 빌드한 것을 검사 해야한다.
로컬 성능측정 결과와 빌드된 결과로 성능을 측정하는 것은 큰 차이가 있다.
로컬 환경에선 최적화되지 않은 코드나 리소스를 사용하기 때문에 실제 사용 환경과는 다를 수 있다.
반면, 프로덕션 빌드에서는 Vite 도구를 사용해 자동으로 리소스를 최적화하고, 압축함으로써 최종 사용자 경험에 가까운 결과를 반영한다.
나는 빌드도구로 Vite를 사용했는데, Vite는 리소스를 gzip으로 압축하는 것을 기본으로 한다.
https://ko.vitejs.dev/config/build-options
따라서, 빌드 기준으로 성능을 측정하자!
# 성능 측정방법
1. npm run build 명령어로 빌드 생성하기
npm run build를 실행하면 아래와 같이 gzip으로 압축하는 것을 알 수 있다.
2. vite preview 명령어로 빌드를 로컬에 띄운다.
https://ko.vitejs.dev/guide/cli.html#vite-preview
3. 시크릿모드에서 Lighthouse 실행하기!
2의 로컬호스트 주소를 시크릿모드 창에서 열고, 개발자 도구의 LightHouse를 실행하자!
LightHouse 옵션을 간단히 정리하면, 다음과 같다.
1. 탐색(Navigation) : 초기 페이지 로딩 시 발생하는 문제 분석
2. 기간(Timespan) : 임의의 시간동안 사용자 인터랙션 측정
3. 스냅샷(snapshot) : 사용자 인터랙션 후 페이지의 상태 측정
Lighthouse의 "탐색(Navigation)" 모드는 브라우저가 페이지를 최초 로드하면서 분석하는,
표준 Lighthouse 동작을 실제로 부르는 이름이다. 이 모드는 페이지의 로딩 성능을 측정하기 위해 사용된다.
출처 : https://ui.toast.com/posts/ko_20211202
나는 메인페이지의 로딩 성능을 측정할 것이기 때문에, 아래와 같이 설정했다!
# 개선 전 측정 결과 먼저 보기!
초기 측정 결과는 다음과 같이 나왔다.
- FCP : 1.8 s
- LCP : 7.3 s
- TBT : 20 ms
- CLS : 0.332
- SI : 1.8 s
"진단" 탭에서는 자세한 문제를 확인할 수 있다.
# 개선 방법 및 시도한 내용들
1. Coverage를 통해 사용되지 않는 코드 제거
cmd + shift + P를 실행해 범위(또는 coverage)라고 입력하고, "적용 범위 보기"를 클릭하면, Coverage 기능을 활용할 수 있다.
새로고침을 하면 아래 이미지처럼 사용량 시각화 그래프를 확인할 수 있고, 사용되지 않는 코드를 확인할 수 있다.
위 Coverage 탭을 보면, index.css에서 사용하지 않은 바이트가 97.2%에 달한다.
사용하지 않는 코드가 97%라는 것이다.
코드를 보니 .mdi 로 시작하는 코드들이 엄청 많았다.
프로젝트에서 Vuetify를 사용하고, material design icon을 MDI-CSS 방식으로 사용하는데,
이 방식은 아이콘을 번들로 가져온다고 한다. (https://vuetifyjs.com/en/features/icon-fonts/#mdi-js-svg)
따라서, MDI-JS SVG 방식을 사용해 필요한 페이지에서만 아이콘을 import 해서 가져오는 방법을 택했다.
이런 수정을 거치고 난 후, 다시 빌드 명령어 실행 후 Coverage 및 Lighthouse를 다시 검사한 결과
Coverage는 97.2% -> 94.8% 로 개선되었고,
Lighthouse는 아래와 같은 결과를 얻을 수 있었다!
(처음에 말했듯 라이트하우스 결과는 시시각각 변경될 수 있어 여러번 테스트 해봤지만 큰 변동이 없었다..)
- FCP : 1.8 s ➡️ 0.8 s (1초 개선)
- LCP : 7.3 s ➡️ 5.3 s (2초 개선)
- TBT : 20 ms ➡️ 20 ms (변동 없음)
- CLS : 0.332 s ➡️ 0.385 s (0.053초 증가..)
- SI : 1.8 s ➡️ 1.0 s (0.8초 개선)
그러나 아직 부족하다...
index.css 파일에서 사용되지 않는 코드를 보니 Vuetify에서 불러오는 color와 관련 코드들이였다.
나의 경우 페이지 내에서 사용하는 컬러가 한정적이여서 전역변수로 두고 사용했고, Vuetify의 color-pack을 전혀 사용하지 않았다.
따라서 Vuetify의 color-pack을 불러오지 않기로 했다.
https://vuetifyjs.com/en/features/sass-variables/#disabling-color-packs
vuetify 공식문서에 나온 것 처럼 utilities와 color-pack을 사용하지 않는 경우
setting.scss에 아래 코드를 적어주자
// setting.scss
@forward 'vuetify/settings' with (
$color-pack: false,
//$utilities: false // 이 코드를 쓰면 Vuetify 쓰는게 의미가 없어지는 것이다.. 잘 확인하고 써야한다!!
);
불필요한 color-pack을 제거했더니,
Coverage는 94.8% -> 83.6% 로 개선되었다.
2. 다이나믹 서브셋 폰트 사용해 LCP(Largest Contentful Paint) 개선하기
LCP란?
콘텐츠가 포함된 최대 페인트 요소
표시 영역에서 가장 큰 콘텐츠 요소가 화면에 렌더링될 때 측정되는 값
LCP가 저하되는 일반적인 원인은 다음과 같다.
1. 느린 서버 응답 시간
2. 자바스크립트와 CSS의 렌더 블로킹
3. 느린 리소스 로딩 시간
4. 클라이언트측 렌더링
web.dev에 따르면 LCP가 저하되는 원인 중 "느린 리소스 로딩 시간"이 있고, 리소스를 줄이기 위한 방법으로는 아래와 같다.
나는 네트워크 탭을 봤을 때 폰트 리소스가 크다고 생각했고,
다이나믹 서브셋 폰트를 사용해 LCP를 5.3초 ➡️ 2.1초 약 3초를 개선했다.
다이나믹 서브셋이란?
다이나믹 서브셋은 CSS의 unicode-range 속성을 이용하여 해당 유니코드 영역의 문자가 사용될 때 브라우저가 폰트 파일를 다운로드 하는 방식을 말합니다. 이를 통해 커다란 통짜 폰트 파일이 아닌, 실제 사용되는 글자가 담긴 폰트 파일만을 다운로드 할 수 있습니다.
출처 : https://leetaewook.github.io/dynamic-subset-font
즉, 다이나믹 서브셋은 즉 문자 전체를 전체 파일이 아니라, 페이지에 포함된 글자만 선택적으로 다운로드할 수 있는 방법이다.
기존에 사용하던 CDN Pretendard 폰트이다.
https://github.com/orioncactus/pretendard
기본 웹폰트를 사용했을 때 3.9MB 크기의 폰트를 불러오고, 다양한 폰트 크기의 파일을 불러오는걸 확인할 수 있다.
// 수정 전 기본 웹폰트
@import url('//cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
반면, 다이나믹 서브셋 폰트를 사용하면 626KB로 크기가 작고, 페이지에 필요한 폰트만 불러오는걸 확인할 수 있다.
// 다이나믹 서브셋 폰트
@font-face {
font-family: 'Pretendard';
font-style: normal;
font-weight: 400;
font-display: fallback;
src:
local('Pretendard'),
url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css')
format('woff2');
}
다시 재빌드 후, Lighthouse를 검사한 결과이다!
3. CLS(Cumulative Layout Shift) 개선하기
우수한 사용자 환경을 제공하려면 사이트의 페이지 방문 중 최소 75% 에 대해 CLS가 0.1 이하여야 한다.
성능 탭을 통해 CLS 레이아웃 변경되는 관련 노드 검사했다.
요약 탭을 확인하면 어떤 노드에서 레이아웃 변경이 일어나는지 확인 할 수 있다.
레이아웃 변경이 일어나는 노드는 Header와 Footer 총 2가지 였다.
Header를 먼저 처리하기 위해 확인해 보았을 때,
헤더 버튼에 height : auto로만 설정되어 있어, 레이아웃 변경이 일어나지 않도록 width, height를 모두 설정해주었다.
이 방식으로 Header는 레이아웃 변경을 막을 수 있었다.
다음은 Footer이다.
성능탭의 레이아웃 변경을 통해 footer에서 레이아웃 변경이 일어나는 것을 알 수 있었다.
프로젝트의 레이아웃 구조는 아래와 같다.
<Header / >
<router-view />
<Footer />
레이아웃이 변경되는 이유는
router-view에서 메인페이지를 로딩하는데 시간이 오래걸려서 Header, Footer를 먼저 렌더링 하고,
메인 콘텐츠가 나중에 렌더링 되면서 Footer가 페이지 제일 아래로 밀려 레이아웃 변경이 일어나는 것이다.
해결방법으로는 Suspense를 사용해 문제를 해결할 수 있었지만,
Vue3에서 Suspense는 아직 실험모드여서 사용하는 것이 불안정해 해결 방법으로 선택하지 않았다.
따라서 app.vue와 Mainview.vue에서 읽어올 수 있는 스토어에 mainLoad 변수를 만들고,
메인페이지가 mounted가 되었다면 mainLoad=true로 변경시켜
mainLoad=true 일 때, Footer를 렌더링 하는 방법으로 해결했다.
// store/app.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAppStore = defineStore('app', () => {
const mainLoad = ref(false) // Main 페이지가 Mounted 되었을 때를 감시하는 변수
const SET_MAINLOAD_STATE = () => { // mainLoad 상태 변경 함수
mainLoad.value = true
return mainLoad.value;
};
return {
mainLoad,
SET_MAINLOAD_STATE
}
})
///////////////////////////////////////////////////////
// MainView.vue
<template>
<div class="main">
<div class="main__content">
<MainCarousel />
< 등등 />
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/store/app';
let { SET_MAINLOAD_STATE } = useAppStore()
onMounted(() => {
SET_MAINLOAD_STATE() // mounted 되었을 때 mainLoad 변수 true로 변경
console.log(mainLoad)
});
</script>
/////////////////////////////////////////////////////////////
// App.vue
<template>
<v-app class="app" :class="displaySize">
<Header />
<router-view />
<Footer v-if="mainLoad"/> // 조건부 Directives를 사용해 Main 페이지가 mounted 되었을 때 Footer 렌더링되도록 한다
<DialogLoading ref="dlg_loading" />
</v-app>
</template>
<script lang="ts" setup>
import Header from '@components/header/Header.vue';
import Footer from '@components/footer/Footer.vue';
import { useAppStore } from '@/store/app';
let { mainLoad } = useAppStore()
</script>
# 최종 결과
이렇게 수정한 결과 아래와 같이 성능이 향상되었다!
처음 측정 결과와 비교해봤다!!
성능 점수가 50점에서 91점으로 개선되면서, 약 82%의 개선을 이루었다!
- 지표 : [개선 전] ➡️ [개선 후]
----------------------------------
- FCP : 1.8 s ➡️ 0.6 s (1.2 초 개선)
- LCP : 7.3 s ➡️ 1.9 s (5.4 초 개선)
- TBT : 20ms ➡️ 20ms (동일)
- CLS : 0.332 ➡️ 0 (0.332 개선)
- SI : 1.8 s ➡️ 0.9 s (0.9초 개선)
아직 LCP가 빨간 글씨인게 마음에 안들지만, 2.5초 이하이므로 적절한 LCP 값이라고 할 수 있다.
좋은 LCP 점수란 무엇인가요?
우수한 사용자 환경을 제공하려면 사이트의 최대 콘텐츠 렌더링 시간이 2.5초 이하여야 합니다.
대부분의 사용자가 이 목표에 도달하도록 하려면 휴대기기와 데스크톱 기기별로 분류된 페이지 로드의 75번째 백분위수로 측정해야 합니다.
출처 : https://web.dev/articles/lcp?hl=ko#what-is-a-good-lcp-score
메인페이지 성능 개선한다고 이런저런 시도를 하면서, 정말 결과에 유효한 영향을 주는 것들만 다시 정리하다보니 시간이 너무 오래걸렸다.
메인 화면 캐러셀의 이미지를 최적화한다면 조금 더 줄지 않을까 싶다!
(imagemin 라이브러리를 통해 이미지를 최적화해보는 시도도 했으나, imagemin을 사용하지 않아도 충분한 개선 효과를 보여서 이 글엔 담지 않았다.)
그럼 끝!
참고 자료
https://devocean.sk.com/blog/techBoardDetail.do?ID=165395&boardType=techBlog
'Vue' 카테고리의 다른 글
Vue3로 돋보기 기능 구현하기(마우스에 따라다니며 확대하기) (2) | 2024.03.28 |
---|---|
[Vue3] watch 사용법(props 변경 감지하기) (0) | 2024.01.16 |
[Vue3, Typescript] Uncaught TypeError: Cannot create property 'value' on number '0' (0) | 2024.01.11 |
[Vue3 Compostion API] I18n 라이브러리를 통한 국제화 및 줄바꿈하기(templete, script내에서 각 사용법) (1) | 2023.12.11 |
[Vue3, Vuetify3, Typescript] Composition API를 사용하여 $refs를 통해 DOM 접근하기(ref 사용법) (2) | 2023.12.06 |