이제 끝났으니 성능 최적화를 돌려보자.
Lighthouse는 크롬 개발자 도구에 있다.
메인 페이지 성능 측정을 돌렸더니...
...?????
왤케 낮음..?
??? 내가 쓰지도 않는 앱이 왜 들어가있음...?
검색해보니 크롬 익스텐션에 깔아두었던 react devtools가 실행되면서 로드되는 모듈이라고 한다.
스택 오버 플로우 해결책
'확장 프로그램 관리'에 들어가 react devtools를 허용 했을 때에만 작동되도록 변경할 수 있다.
위 사항을 수정한 후 다시 측정한 점수이다.
이미지를 많이 쓰는 사이트인 만큼
이미지 사이즈에 대한 성능 최적화가 부족하다고 나온다.
tmdb는 backdrop path 이미지를 4가지로 제공한다. 참조
"backdrop_sizes": [
"w300",
"w780",
"w1280",
"original"
],
현재 https://image.tmdb.org/t/p/w780/${backdrop_path}
을 사용하고 있는데, movieRow에 대해서 https://image.tmdb.org/t/p/w300/${backdrop_path}
으로 바꾸어 주었다.
점수가 올랐고, 부적절한 사이즈라는 내용도 사라졌다.
현재 4k모니터라 사이즈가 줄은 것이 약간 티가 나는데, 4k모니터에서 이정도면 일반적인 환경에서는 티가 안날 것 같다.
그리고 tmdb의 cdn에서는 jpeg대신 webp를 반환하기에 별도의 이미지 용량 최적화는 필요하지 않다.
static 파일에는 next/image를 이용하여 최적화를 할 수 있다.
<img
className="mr-3 rounded-circle"
src={`${image}`}
alt="profie image"
style={{ maxWidth: "50px" }}
/>
<Image
className="mr-3 rounded-circle"
width={50}
height={50}
src={`${image}`}
alt="profie image"
style={{ maxWidth: "50px" }}
/>
<img
alt="logo"
src="/logo.png"
className="nav__logo mt-1"
role="button"
onClick={() => router.push("/main")}
/>
<Image
height={40}
width={120}
alt="logo"
src="/logo.png"
className="nav__logo mt-1"
role="button"
onClick={() => router.push("/main")}
/>
<html> element must have a lang attribute
HTML 태그에 lang이 없다고 감점당했다.
_document.html
을 아래와 같이 수정해준다.
render() {
return (
<Html lang="ko">
위의 성능 측정 결과가 뒤죽박죽한데,
구글 웹폰트에서 폰트를 받아오는 서버 반응속도에 따라 달라지는 것 같다.
폰트를 static으로 바꾸자.
현재 디자인에서 사용되는 폰트 웨이트는 400, 500, 600, 700 사이즈이다.
400과 700으로 통일시키기로 했다.
그리고 https://transfonter.org/에서 웹 폰트를 만들 수 있는데...한글 서브셋이 없넹?
nonria님 블로그에서 한글 subset을 찾을 수 있었다.
https://nonria.com/post/104/
400은 regular 사이즈 700은 bold 사이즈이다.
public/fonts 폴더에 해당 파일을 넣는다.
globalStyle에 아래와 같이 넣어준다.
import { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 400;
src: url('/fonts/NotoSansKR-Regular.woff2') format("woff2"),
url('/fonts/NotoSansKR-Regular.otf') format('opentype');
font-display: swap;
}
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 700;
src: url('/fonts/NotoSansKR-Bold.woff2') format("woff2"),
url('/fonts/NotoSansKR-Bold.otf') format('opentype');
font-display: swap;
}
https://developer.chrome.com/ko/docs/lighthouse/performance/font-display/의 권고대로 swap을 넣어 foit를 방지했고, 하나의 폰트패밀리 Noto Sans KR로 2 개의 굵기가 결합되게 된다.
이로서 3메가가 넘었던 웹폰트가 woff2사용시 300kb, otf사용시 900kb로 줄어들었다.
그 밖에 'Largest Contentful Paint' 점수가 낮아지는 경우도 있었으나, tmdb에서 이미지를 늦게 보내주는 경우로,
본 프로젝트에서는 랜딩페이지에서 미리 이미지를 프로로딩하기 때문에 실제 사용에서는 문제가 되지 않을 것으로 보인다.
그 밖에 부트스트랩의 경우 24kb를 차지하고 있고, 최적화를 하면 4kb까지 줄어들 수 있는데, 1점 정도 성능차이가 날 것 같아서(그리고 purgeCss를 next js에 사용하는 법이 너무 복잡해서) 최적화를 하지 않기로 하였다.
대신 부트스트랩에서 자바스크립트를 사용되는 부분이 modal 밖에 없었고, 이에 따라 모달을 다른 디자인으로 대체하고 부트스트랩의 js파일은 로드하지 않도록 수정하였다.
tmdb의 응답이 빠른 경우
tmdb의 응답이 느린 경우.
tmdb에서 사진을 모두 다 불러오다 보니까 tmdb의 응답 속도에 따라서 LCP 속도가 크게 차이나는 것을 확인할 수 있다....ㅜㅜㅜ
이를 최적화하기 위해서는 빌드시에 사진들을 static으로 다운받을 후, next image로 사이즈에 맞게 변환한 후, 사용자에게 전달해야 한다.
너무 번거롭기도 하고, next-image-optimizer라는 외부 라이브러리를 이용해야 해서 될지 안될지 장담이 안된다. (최초 접속자의 TTI가 치솟는 것은 당연하고...)
현재로서는 TTI를 1.1초로 맞추었다는 점, LCP를 랜딩페이지의 preload로 해결했다는 점에서 만족해야 겠다.
위에서는 만족한다고 했지만, 아무래도 이미지를 최적화하고 싶은 욕심이 들었다.
본 프로젝트에서는 TMDB를 통해서 이미지를 받아오기 때문에 이미지 최적화에 제약이 많았다.
특히 가장 문제가 된 부분은 모달창이었다.
onMouseEnter가 발생될 때 이미지를 preload 시킴에도 불구하고, 이미지가 로딩되는 시간이 1초 이상씩 걸리는 경우가 많아 preload가 무의미한 수준이었다.
모달창 부분만 sharp라는 라이브러리를 이용하여 최적화 시켜보려고 한다.
chatGPT의 도움을 많이 받았다.
yarn node-fetch sharp
import fetch from "node-fetch"
import sharp from "sharp";
import fs from "fs";
import path from "path";
export async function getStaticProps({ params: { movieId } }) {
const { data: movie } = await getMovieDetail(movieId);
const res = await fetch(
`https://image.tmdb.org/t/p/w1280/${movie.backdrop_path}`,
);
const buffer = await res.arrayBuffer();
const optimizedImage = await sharp(Buffer.from(buffer))
.jpeg({ quality: 60, mozjpeg: true })
.toBuffer();
const filePath = path.resolve(
path.join(process.cwd(), "public", "backdrop", movie.backdrop_path),
);
fs.writeFileSync(filePath, optimizedImage);
...
fetch는 브라우저에서 요청을 보내는 그 fetch가 맞다. node-fetch는 이 fetch 요청을 node.js에서 사용하도록 도와준다.
arrayBuffer는 이진 형식의 배열이다. 이 배열을 sharp라는 라이브러리에 던져주면, sharp가 순차적으로 이미지를 변환해준다.
sharp는 인기있는 자바스크립트 이미지 라이브러리이다. .resize({width:500}), .jpeg(), .webp() 등의 메서드로 이미지의 확장자, 퀄리티 등을 변환한다.
.toBuffer로 버퍼 형식으로 내보낼 수 있으며, 결과적으로 이렇게 버퍼 형식으로 내보내는 파일을 writeFileSync로 실시간으로 파일로 변환하게 된다.
const response = await axios.get(
`https://image.tmdb.org/t/p/w1280/${movie.backdrop_path}`,
{
responseType: "arraybuffer",
},
);
const buffer = Buffer.from(response.data);
const optimizedImage = await sharp(Buffer.from(buffer))
.jpeg({ quality: 60, mozjpeg: true })
.toBuffer();
const filePath = path.resolve(
path.join(process.cwd(), "public", "backdrop", movie.backdrop_path),
);
fs.writeFileSync(filePath, optimizedImage);
CORS 오류를 해결하기 위해 노력했지만, 근본적으로 getInitialProps에서 axios, fetch 요청을 보내면 에러를 피할 수 없는 것으로 보인다. 추후에 next js의 api routes를 학습하게 되면 수정해야 겠다.
이미지의 사이즈를 줄이면 이미지가 저화질 이미지인 것이 바로 눈에 띄었다. 따라서 size 옵션은 제외하고, 대신 이미지의 quality를 60까지 낮추었다.
webp와 jpeg, mozjpeg를 비교하였다.
jpeg는 50kb였다.
mozjpeg는 35kb였다.
webp는 22kb였다.
결과적으로 webp가 제일 적절...
해보이지만 webp를 사용하게 되면 호환성 문제가 있기에 기존 img태그를 picture태그로 모두 바꾸고, webp와 jpeg를 모두 추가해주고, build타임에도 두 이미지를 모두 다운받고 저장해야 한다.
서버에 두 개의 이미지를 저장할만큼의 차이가 없다고 생각되어
결과적으로 mozjpeg를 사용하는 것이 적해 보였다.
CORS 이슈로 결국엔 적용하지 못했지만, 적용하게 되면 활용할 수 있는 용도가 많아보인다.
공부해 두었다가 다른 프로젝트에서 써야지.
https://www.npmjs.com/package/@next/bundle-analyzer
next-js에서 번들 아날라이저를 쉽게 사용할 수 있는 법이 있어 적으려고 한다.
yarn add -D @next/bundle-analyzer
로 @next/bundle-analyzer를 설치한다.
next.config.js를 아래와 같이 수정한다
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
const nextConfig = {
reactStrictMode: false,
compiler: {
styledComponents: true,
},
};
module.exports = withBundleAnalyzer(nextConfig);
ANALYZE=true yarn build
와 같이 환경변수를 추가하여 build 를 해주면 번들 분석 결과를 같이 보여준다.
생각보다 파일들이 잘 chunk된 것을 확인할 수 있다.
가장 클거라 예상은 했었던 firestore도 생각보다 크지 않았다.
추후에 기회가 된다면 8버전 문법을 제거하고 9버전의 모듈형 문법으로 변경하여 firestore를 codespliting해야겠다.