Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[성능 베이스캠프] 지그(송지은) 미션 제출합니다. #10

Merged
merged 28 commits into from
Sep 2, 2021

Conversation

zigsong
Copy link
Contributor

@zigsong zigsong commented Aug 30, 2021

🔥 결과

  • 배포한 CloudFront 접근 경로: https://d3sydo67idcl5c.cloudfront.net/

  • 개선 전후 성능 측정 결과

    • 개선 전 (S3)

    • 개선 후 (CloudFront)

  • Lighthouse 95점 이상 → 위 사진

  • Home 페이지에서 불러오는 스크립트 리소스 크기 < 60kb

  • 히어로 이미지 크기 < 120kb

  • 프랑스 파리에서 Fast 3G 환경으로 접속했을 때

    • Home 첫 번째 로드시 LCP < 2.5s
    • Home 두 번째 이후 로드시 LCP < 1.5s

✅ 개선 작업 목록

1 요청 크기 줄이기

  • 소스코드 크기 줄이기

    • gzip 압축: 797kB → 137kB
    • CSS 최적화: 797kB → 757kB
      CssMinimizerWebpackPlugin + MiniCssExtractPlugin
      - CssMinimizerWebpackPlugin - CSS 파일을 최적화하고 축소해준다
      - MiniCssExtractPlugin - CSS 파일을 별도 파일로 추출하여, CSS 코드가 포함된 JS 파일 별로 CSS 파일을 생성한다. (CSS 및 SourceMaps의 온 디멘드 로딩기능도 지원한다.)
      
    • JS 소스코드 난독화 (code split 이후 실행)
      • TerserWebpackPlugin 사용 (UglifyWebpackPlugin과 압축 후 사이즈는 거의 일치하지만, 파싱 속도가 더 빠르다고 한다.)

  • 이미지 크기 줄이기

    • pngwebp, gifmp4

2 필요한 것만 요청하기

3 같은 건 매번 새로 요청하지 않기

  • GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.
    • 객체를 이용한 cache 구현
  • CloudFront 캐시 설정
    • 기본 캐시 정책 사용
    • 바뀔 일이 거의 없는 정적 파일들에 대해서는 Cache-Control: max-age 헤더 추가

4 최소한의 변경만 일으키기

  • 검색 결과 > 추가 로드시 추가된 목록만 렌더되어야 한다.
    • GifItem을 React.memo로 래핑
      <memoize 이전> 모든 GifItem이 리렌더링

      <memoize 이후> 기존의 GifItem은 리렌더링이 되지 않음
  • LayoutShift 없이 hover 애니메이션이 일어나야 한다.
    • top 속성 대신 transform: translate 사용

🧐 공유

  • ImageMinifierPlugin은 왜 compression이 거의 되지 않을까? 어떤 옵션과 함께 사용해야 할까?
  • React.lazy vs loadable components
    • Loadable components는 preload가 가능하다. lazy와 loadable, 각각을 언제 쓰면 좋을지 고민해봐야겠다.
  • Uglify vs Terser
    Terser vs. Uglify vs. babel-minify: Comparing JavaScript minifiers - LogRocket Blog
    • Terser의 parse 속도가 uglify보다 더 빠르다고 한다!
  • cache를 객체로 저장하는 것과 localStorage에 저장하는 것 중 어떤 것이 더 좋은 방법일까?
    • WeakMap은 딱히 소용이 없을까? 우선 딱히 키값으로 객체를 사용할 이유가 없다고 생각하여 그냥 Map을 사용했다. WeakMap의 키로 사용된 객체가 없다면 해당 객체는 GC에 의해 수거된다는데, 어느 상황에서 유용하게 쓰일 수 있을지 궁금하다.
  • MiniCssExtractPlugin 을 사용하여 CSS 파일을 분리하는 것이 모든 경우 더 바람직할까?
  • webp는 상당히 혁신적인 확장자인 것 같다. gif를 지양해야 하는 이유에 대해서도 아티클을 제대로 읽어봐야겠다.
  • github actions에서 cloudfront cache invalidation을 위한 aws cli 명령어가 먹히지 않는다. 😞

💎 성능 미션 정리 블로그

Copy link
Contributor Author

@zigsong zigsong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인덴트 다 줄인 건데 왜 늘어났는지 모를 일이네요 😭

Comment on lines 7 to 14
<div className={styles.featureItem}>
<video className={styles.featureImage} autoPlay muted loop>
<source src={videoSrc} type="video/mp4" />
Sorry, your browser doesn't support embedded videos.
</video>
<div className={styles.featureTitleBg}></div>
<h4 className={styles.featureTitle}>{title}</h4>
</div>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gifmp4로 변경

Comment on lines 6 to 10
return (
<footer>
<p>memegle 2021. All rights reserved. Powered by <a href="https://giphy.com/">Giphy</a>.</p>
</footer>
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오잉 이건 인덴트를 4 → 2로 줄인건데 왜 더 들어간 것처럼 됐지

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인덴트가 space 2칸이 아니라 tab 2칸인 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 글쿠나 고마워용

export default React.memo(GifItem);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GifItem memoization

@@ -41,6 +41,6 @@
/* PC */
@media all and (min-width:1080px) {
.gifItem:hover {
top: -0.75rem;
transform: translateY(-0.75rem);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layout shift 방지

<span className={styles.logo}>memegle</span>
</Link>
<Link to="/search">
<button className={styles.searchPageButton} onMouseOver={() => SearchLoadable.preload()}>start search</button>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search 컴포넌트에 loadable preload 적용

<span>We Found...</span>
</h4>;
if (noResult) {
return <h4 className={styles.resultTitle}><span>Nothing</span>🥲</h4>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

노팅 아니고 낫띵!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

Comment on lines 21 to 39
const cache = { current: null };

return async () => {
try {
if (!cache.current) {
const response = await fetch(TRENDING_GIF_API);
const gifs = await response.json();
const data = formatResponse(gifs.data ?? []);

cache.current = data;
}

return cache.current ?? [];
} catch (error) {
return [];
}
};
})();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클로저 리턴하여 객체(cache)값 지키기!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const cache = { current: null };
과 같이 작성한 이유가 있는지 궁금합니다(순수한 호기심)

useRef 와 비슷하게 사용된다는 뉘앙스를 주기 위해서일까요~?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bigsaigon333
사실 @Tanney-102 브콜의 코드가 좋아보여서 참고했습니다! 다르게 하려고 했는데 current라는 네이밍이 탐나더라구요 ㅋㅋㅋ
저는 useRef와 비슷한 뉘앙스라고 생각하고 사용했습니다 😊

Copy link

@Tanney-102 Tanney-102 Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bigsaigon333 @zigsong 덧붙이자면 추후 코드를 고도화할때 추가 정보를 넣을 수 있겠다 생각했습니다 (최초 캐시 시간, 캐싱 기간 등) 여러 변수로 관리하기 보다 하나의 객체로 관리하는게 좋다 생각했어요!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클로저를 잘 적용하신 것 같습니다 👍👍👍 멋지네요 ㅎㅎ
현재는 fetchTrendingGifs에만 캐싱이 적용되어 있는데 만약 다른 api 요청에도 캐싱이 적용되야 한다면 어떨까요?
한단계 더 추상화하여 캐싱이 적용된 함수를 리턴하는 함수가 있다면 엄청 fancy 할 것 같습니다 😄

Comment on lines 14 to 16
filename: '[name].js',
path: path.join(__dirname, '/dist'),
clean: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code splitting용 청크 분리

Comment on lines +31 to +35
process.env.GIPHY_API_KEY
? new DefinePlugin({
'process.env.GIPHY_API_KEY': JSON.stringify(process.env.GIPHY_API_KEY),
})
: new Dotenv(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰어 곤이가 알려준 환경변수 정의하기

Comment on lines +61 to +62
minimizer: [new CssMinimizerPlugin(), new TerserPlugin()],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimize 켜주기! (true)

Copy link

@yungo1846 yungo1846 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 미처 신경쓰지 못하고 넘어간 부분들(max-age, CSS chunk 분리) 등을 잘 적용해주셔서 많이 배울 수 있었습니다 👍

성능최적화에 대해 정리하신 블로그 글도 엄청 깔끔하게 잘 쓰셨더라고요. 지그의 정리 실력을 닮고 싶네요 😄

그런데 vscode의 인덴트 설정이 잘 못 되어 있는지 코드의 인덴트가 엄청 깊었습니다 ㅎㅎ;;
한번 확인해주시면 좋을 것 같아요...!

추가적으로 남긴 코멘트도 한번 고민해보시면 좋을 것 같습니다 👍

Comment on lines 15 to 16
<Link to="/search">
<button className={styles.searchPageButton} onMouseOver={() => SearchLoadable.preload()}>start search</button>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onMouseOver={() => SearchLoadable.preload()} 이 부분은 익명함수를 안 써도 괜찮을 것 같아요!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

익명함수를 안쓰면 이벤트 객체가 preload의 매개변수로 전달되지 않나요..? 👀
(Loadable Components 안써봐서 걍 궁금해서 물어봅니다 🐕 )

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preload에서 인자를 쓰지 않으니 이벤트 객체가 넘어가도 상관없다고 판단했습니다 ㅎㅎ

Comment on lines 24 to 25
</div>
<Link to="/search" onMouseOver={() => SearchLoadable.preload()}><button className={styles.cta}>start search</button></Link>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 마찬가지로 익명함수 없이 해결할 수 있을 것 같습니다!

Comment on lines +4 to +6
const SearchLoadable = loadable(() => import("../Search/Search"), {
fallback: <div>Loading...</div>
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback을 주셨군요!
fallback이 뜨는 상황일 때, 지금의 코드에서는 일시적으로 흰색 배경이 뜰 것 같아요. memegle의 기본적인 배경색이 검은색이니까 사용자 입장에서는 눈이 조금 아플 것 같습니다 😂

지그도 이를 잘 알고 대비책으로 마우스 엔터시에 프리로드를 주셔서 해결하셨네요 👍
하지만 사용자가 tab 키를 이용하여 start review 버튼을 누른다면 깜빡이는 현상이 발생하지 않을까요..? 🤔

Comment on lines 21 to 39
const cache = { current: null };

return async () => {
try {
if (!cache.current) {
const response = await fetch(TRENDING_GIF_API);
const gifs = await response.json();
const data = formatResponse(gifs.data ?? []);

cache.current = data;
}

return cache.current ?? [];
} catch (error) {
return [];
}
};
})();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클로저를 잘 적용하신 것 같습니다 👍👍👍 멋지네요 ㅎㅎ
현재는 fetchTrendingGifs에만 캐싱이 적용되어 있는데 만약 다른 api 요청에도 캐싱이 적용되야 한다면 어떨까요?
한단계 더 추상화하여 캐싱이 적용된 함수를 리턴하는 함수가 있다면 엄청 fancy 할 것 같습니다 😄

Comment on lines 81 to 90
useEffect(async () => {
if (loading) {
const gifs = await fetchTrendingGifs();

setGifList(gifs);
setLoading(false);
}

return () => setLoading(true);
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 놓치고 있던 부분인데 공원의 함정이 있었네요 ㅋㅋㅋㅋ
도비 덕에 알았습니다. useEffect의 콜백이 async 함수였네요 ㅎㅎ;;;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오마이갓

@zigsong
Copy link
Contributor Author

zigsong commented Sep 2, 2021

안녕하세요 곤이~!

  1. loading 컴포넌트 만들었습니다 하하 예쁘죠

  1. memoizePromise 곤이 PR 참고해서 만들었어요~! a2462e7

  2. useEffect 안의 async도 해결했습니다 알려주셔서 고마와용 23b1e7b

@yungo1846
Copy link

넵 지그 리뷰 반영해주신 것 확인했습니다!
이번 미션 진행하면서 성능최적화에 대해 새롭게 많이 배워가신 것 같네요 👍
로딩 페이지도 엄청 깔쌈하게 뽑혔네요 ㅎㅎ

바로 merge 하겠습니다!

@yungo1846 yungo1846 merged commit 9a1b6f4 into woowacourse:zigsong Sep 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants