구글 PageSpeed 성능 개선하기(무한 스크롤, preload, 이미지 형식변환, 이미지 압축...)
구글에는 PageSpeed Insight 라는 페이지 성능과 웹접근성 등을 확인할 수 있는 기능이 있다.
해당 기능을 활용하면 점수 뿐만 아니라 권장사항도 자세하게 알려주기 때문에 정말 유용하다.
PageSpeed Insights
올바른 URL을 입력하세요.
pagespeed.web.dev
이번에 웹페이지를 배포하게 되면서 실제 유저도 생겼다. 따라서 유저들이 불편함 없이 페이지를 사용하는지 궁금해서 페이지 성능에 관심이 갔다. 그걸 정량적으로 파악할 수 있는 지표가 이 기능이라고 판단하여 성능 체크를 해보고 개선도 해보았다.
나는 페이지를 종합적으로 평가해서 점수를 내리는 줄 알았는데 페이지 라우터별로 입력해서 점수를 볼 수 있다.
근데 어찌보면 그게 당연하기도 하고 또 세부적인 성능 개선 사항을 알 수 있으니 좋은 것 같다.
일단 처음의 결과는 처참했다 ㅎㅎ
주요 페이지인 후기 페이지를 먼저 측정해보았다. 성능이 50점 미만이면 '나쁨'인데 간신히 50점에 걸쳐 '개선' 단계로 된 상황이었다. 그래도 데스크탑 점수는 80점대인데 모바일이 특히 성능이 낮았다.
그럼 이제 왜 이렇게 점수가 낮은지 파악해보기로 했다.
보니까 LCP(콘텐츠가 포함된 최대 페인트) 점수가 0점이었다. 그러니까 렌더링이 가장 오래 걸리는 요소가 렌더링 되는 시간이 권장 시간보다 오래 걸리는 것이 주요 원인이었다.
1. LCP란?
LCP는 사용자가 처음 페이지로 이동한 시점을 기준으로 표시 영역에 표시되는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간을 말한다. 이 시간이 2.5초 미만이면 가장 좋고 4초 미만이면 권장, 그 이후부터는 나쁨이다.
나는 권장 사항 4초를 훨씬 넘는 21초였다..
해당 관련 문서를 보면 LCP로 고려되는 요소는 img 요소, video 요소, 텍스트 요소 등이라고 한다.
후기 페이지는 LCP가 이미지였는데 많은 이미지가 렌더링되기 때문에 특히 오래 걸리고 있었다.
2. 무한 스크롤 적용
그래서 나는 api 로 받아오는 데이터를 나눠서 받아오기로 했다.
그렇게 하면 좀더 빠르게 데이터를 받아와서 렌더링 속도를 단축할 수 있을 것이라 판단했다.
이것을 위해 많이들 사용하는 '무한 스크롤' 방법을 사용하여 뷰포트에서 페이지 이동을 감시하기로 했다.
무한 스크롤을 구현하기 위한 방법 또한 다양하게 있지만 보편적으로 많이 사용하는 Intersection Observer API 를 사용해서 구현했다.
공식문서에서 Intersection Observer API는 '브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지, 더 쉽게는 사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 제공합니다.' 라고 되어있다.
new IntersectionObserver()를 통해 생성한 인스턴스로 관찰자를 초기화하고 관찰할 대상을 정하였다.
생성자는 2개의 인수를 갖는다.(callback,options)
여기서 callback 함수는 설정한 뷰포트와 요소가 교차할 때, 즉 사용자 화면에 설정한 요소가 보이지 않을 때 실행할 함수이다. 그러니까 이 함수를 api 호출하는 함수로 지정하면 스크롤을 통해 요소가 교차할 때마다 api를 호출할 수 있다.
options는 뷰포트의 영역과 여유 마진 등을 지정한다.
더 자세한 내용은 공식문서를 참고하면 된다.
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Intersection Observer API - Web APIs | MDN
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
developer.mozilla.org
해당 방법대로 잘 구현된 것을 확인할 수 있다.
뷰포트에서 벗어나면 추가로 15개씩 게시물을 불러온다. 참고로 현재 게시물이 많지 않아 5개만 호출되었다.
이렇게 했는데 2점 향상으로 기대에 못미치는 수준이었다. 그래서 다른 방법을 생각해보았다.
3. 폰트, 이미지 preload
LCP 최적화 권장사항을 확인해보았는데 '콘텐츠가 포함된 최대 페인트 이미지 미리 로드' 라는 문구가 눈에 띄었다.
미리 로드에 대해서 좀더 알아보았다.
https://web.dev/articles/optimize-lcp?hl=ko
최대 콘텐츠 렌더링 시간 최적화 | Articles | web.dev
LCP 개선을 위한 핵심 영역을 파악하고 이해하기 위한 단계별 가이드입니다.
web.dev
이 글에서 확인해보니 script 요소는 html,css가 로드된 이후에 로드가 된다. 이것으로 인해 렌더링 속도가 지연될 수 있고 이를 개선하기 위해서는 preload 속성으로 LCP요소를 미리 로드할 수 있다고 한다.
후기 페이지에서 이미지는 동적으로(api에서) 받아오기 때문에 미리 로드할 수는 없고, api의 후기 이미지가 없을 때 로드되는 기본 아이콘을 preload 하기로 했다.
폰트 preload로 성능을 올렸다는 글도 봐서 폰트도 preload 하였다.
index.html 내에 프리로드 해주었다.
폰트 preload 이후 성능이 5점 올랐다!
이제 이미지들을 미리 로드해보기로 했다.
먼저 이미지 프리로드에 사용할 훅 파일을 만들어줌
최대 페인트 이미지 미리 로드해줌
그런데 이렇게 했더니 무한스크롤이 동작하지 않았다. 초기 렌더링했던 컴포넌트가 그대로 렌더링되는듯 했다.
이렇게 했더니 콘솔에 관찰이라는 텍스트가 뜨지 않는걸로 보아 최초에 loading이 렌더링되면서 observer에 해당하는 요소가 잡히지 않는 것이 원인이었다.
따라서 일단 loading 텍스트를 조건부로 띄우지 않고 테스트해보기로 했다.
다른 정적 이미지들과 함께 로드되는 것을 볼 수 있다. 그리고 시작점이 만들어둔 useImagePreload 훅에서 잘 실행되고 있다.
성능이 5점 더 올랐다!
그래도 아직 부족하기 때문에 더 끌어올릴 수 있는 방법을 생각하기 위해 권장사항을 더 자세히 읽어보았다.
4. 이미지 확장자 변환, 압축
LCP 비율에서 로드 지연이 87프로로 대부분의 비율을 차지하고 있었다. 이상적인 비율과 완전히 다르다.
뭔가 잘못되었다 싶어 자세히 알아보기로 넘어가 읽어보았다.
리소스 로드 지연이 발생하는 요소를 읽어보았다. 그중 '페이지에 동적으로 추가된 <img> 라는 부분이 눈에 띄었다. 내 코드를 다시한번 살펴보니 정확하게 이미지 태그가 동적으로 추가되고 있었다.
데이터에 이미지가 있는지 여부에 따라 다르게 렌더링되고 있었다. 그러니까 api호출 이후에 판단을 통해 img태그를 추가하므로 지연이 느리게 되고 있는 것이었다.
따라서 부모 컴포넌트에 이미지 src가 있는지 여부를 판단하는 함수를 하나 만들어서 넘겨주었다.
태그를 동적으로 하지 않고 src속성과 alt속성 내의 string 을 동적으로 제어하기로 했다. 하위 컴포넌트에서 해당 함수를 실행했을 때 반환값에 따라 불러온 이미지를 쓸지 기본 이미지를 쓸지가 결정된다.
근데 이렇게 했더니 다행히 리소스 로드 지연은 비율이 줄었지만 그 비율이 고스란히 요소 렌더링 지연으로 갔다. 그리고 오히려 성능이 더 떨어졌다.
결국 문제를 해결하려면 이미지를 빠르게 불러오는 방법밖에 없는 것 같았다. 그래서 기본 이미지의 확장자를 변환하고, 업로드 이미지를 압축해주기로 했다.
convertio를 이용해 이미지의 확장자를 svg에서 webp로 변환해주었더니 6KB에서 3.79KB로 변환된 모습이다.
이제 업로드 할때 자동으로 변환해서 서버에 넘겨주는 라이브러리를 적용해보려고 한다.
browser-image-compression 이라는 이미지 압축 라이브러리에서 compressionImage 라는 함수를 불러와서 이미지를 압축했다. 이제 후기를 등록할 때는 이미지를 압축해서 보냈다. 이 함수에는 매개변수 imgUrl과 options 가 있는데 url은 변환하려는 이미지 url이고 options는 허용하는 최대 이미지 크기를 지정할 수 있다. 나는 1920 픽셀로 하였다.
그러나 이것을 적용해도 아쉽게 점수가 달라지지 않았다.
아무래도 api로 동적으로 불러오는 게 주된 원인인 것 같다. 그렇지만 api 로 불러오기 전에는 이미지를 미리 render할 수 없으니 개선방법을 잘 모르겠다.
추후 서버사이드 렌더링 등 다른 방법을 고려해서 더 좋은 성능으로 개선해보고 싶다.