-
Notifications
You must be signed in to change notification settings - Fork 166
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
[개인 미션 - 성능 오답노트] 해리(최현웅) 미션 제출합니다. #151
Conversation
- css 크기를 줄이고, 번들링 된 자바스크립트 파일에서 분리하기 위한 웹팩 도구 추가 - 이미지 크기를 최적화 하기 위한 웹팩 도구 추가 - 번들링되는 파일의 크기를 비교하기 위한 웹팩 도구 추가 - css sideEffect를 예방하기 위한 설정 추가 - ESM 구문을 사용하기 위해 modules:false 설정
- reflow를 발생시키는 css 속성을 제거하고 translate 속성을 사용
- 파일 내용이 변경될 때마다 파일 이름이 변경되도록 contenthash 사용 - 번들 사이즈 분석을 위한 플러그인 추가 - css를 js파일에서 분리하기 위한 플러그인 추가 - css 파일 크기 최적화를 위한 플러그인 추가 - 이미지, gif를 webp로 변경하기 위한 플러그인 추가 - 중복되는 코드 덩어리를 하나의 파일로 추출하기 위한 splitChunk 프로퍼티 추가
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요~ 해리!
“쑤쑤나는해리야” 아주 잘~ 봤습니다~ 저희 나이대에 인재가 참 많네요^,^~
대체적으로 요구사항을 모두 만족하신 것 같아서 몇 가지 코멘트 달았습니다! 그리고 폰트 preload도 로딩 속도 측면에서 은근히 차지하는 비율이 높아서 관련 문서 참고하시면 좋을 것 같아요!
추가로 해리 PR을 읽고 느꼈던 건데 궁금증을 가지고 접근하는 방식이 되게 좋았어요. PR 읽으면서 저도 많이 배운 것 같아요.. 그리고!!!! 공식 문서를 많이 참고하시는 것 같아서 공식 문서가 오히려 딱딱하고 이해가 잘 되질 않는 제 입장에서는 부러웠답니다🥹 공식 문서 읽는 팁이 있다면 알려주시면 감사하겠습니다~
package.json
Outdated
"license": "MIT", | ||
"dependencies": { | ||
"@giphy/js-fetch-api": "^4.1.1", | ||
"@giphy/js-types": "^4.2.1", | ||
"classnames": "^2.3.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-icons": "^4.4.0", | ||
"react-icons": "5.3.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usedExports
옵션을 각각 true/false
로 주고 빌드해보면 사이즈 차이가 꽤나 커요. react-icons
에서도 tree shaking
이 적용이 되고 있다는 얘기겠죠.
적용은 되고 있지만 완벽하게 되는지? 확실히 모르겠지만 사용되는 아이콘 뿐만 아니라 모든 아이콘을 불러오는 것 같아요. 관련 이슈 참고 바랄게요.
React Icons Imports everything even when included 2 or 3 icons #154
The chunk is very big. #289
기존 react-icons
대신 react-icons/all-files
를 이용하니 Gzipped size 기준으로 0.45 KB가 줄었네요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쑤쑤 정성 가득한 피드백 정말 감사합니다~ 미션을 진행하면서도
@react-icons/all-files 를 설치할 필요가 있을까? 에 대해서 정말 많이 고민을 했었습니다. 번들 사이즈 관점에서 큰 차이가 없는데 굳이 사용할 필요가 없을 것 같다고 판단해서 사용하지 않았었는데요. 쑤쑤의 피드백을 통해서 다시 한 번 도입 필요성에 대해서 생각해 보게 되었어요. 그 후, 생각을 바꾸고 @react-icons/all-files
라이브러리를 설치하기로 했습니다.
그 이유는 아래와 같아요.
1. 불안정성
쑤쑤가 첨부해 준 링크에 들어가서 이슈와 코멘트 내용들을 확인해 보니, 빌드 시 트리 쉐이킹 관련해서 문제가 100% 해결되지 않은 것을 알 수 있었어요.
2. 프로젝트 규모가 커져 아이콘을 많이 사용할 경우
프로젝트 규모가 커지고 라이브러리에서 사용하는 아이콘도 점점 많아질수록 1KB 이내의 차이라도 사용자에게 조금이라도 적은 번들 사이즈를 다운받도록 하는 것이 사용자 경험 측면에서 더 좋을 것 같았어요.
그래서 설치하기로 변경했답니다^^
- 설치 전:
1.72KB
- 설치 후:
1.28KB
0.44KB
정도 줄어들었네요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
단순히 번들 사이즈가 줄어서 설치하는게 아닌 구체적인 이유를 함께 적어주셔서 좋네요👍
src/App.tsx
Outdated
<Router> | ||
<Suspense fallback={<div>loading...please wait...</div>}> | ||
<NavBar /> | ||
<Routes> | ||
<Route path="/" element={<Home />} /> | ||
<Route path="/search" element={<Search />} /> | ||
</Routes> | ||
<Footer /> | ||
</Suspense> | ||
</Router> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NavBar와 Footer도 Suspense로 감싼 이유가 궁금합니다! NavBar와 Footer는 Home이나 Search보다 빠르게 로드되니까 Suspense로 감싸게 되면 오히려 렌더링이 늦어지는 단점이 존재할 것 같은데 해리는 어떻게 생각하시나요🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시 Layout Shift 때문에 NavBar와 Footer도 Suspense로 감싸신 거라면 Layout Shift가 일어나지 않게 로딩 화면이 화면 전체를 차지하게 만들면 일어나지 않아요. 다만 이러면 Home 페이지에서도 lazy를 적용하니까 첫 페이지(Home)에 들어갔을 때, 로딩 시간이 걸린다는 단점이 있을 수 있겠네요😭
const App = () => {
return (
<Router>
<NavBar />
<Suspense fallback={<div style={{ width: '100vw', height: '100vh' }}>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
</Routes>
</Suspense>
<Footer />
</Router>
);
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쑤쑤 정말 좋은 피드백 감사합니다! 😆
NavBar와 Footer도 Suspense로 감싼 이유가 궁금합니다! NavBar와 Footer는 Home이나 Search보다 빠르게 로드되니까 Suspense로 감싸게 되면 오히려 렌더링이 늦어지는 단점이 존재할 것 같은데 해리는 어떻게 생각하시나요🤔
저도 그럴 것 같다고 생각합니다! Suspense는 UI를 그릴 준비가 완료될 때까지 fallback UI를 보여주므로 현재 저와 같이 App.tsx
를 구성하면 NavBar, Footer 컴포넌트는 UI를 그릴 준비가 완료되었지만 Home
컴포넌트를 다운로드 받는 동안 fallback UI를 보여주게 되는 문제가 생기겠네요! 네트워크 환경이 좋지 못한 사용자라면 특히 더 불편함을 느낄 수 있을 것 같아요.
Layout Shift 현상을 줄이기 위해서 일단 모든 컴포넌트를 Suspense로 감싸야겠다고 판단한 것도 맞습니다. 쑤쑤의 피드백! 반영해 보도록 하겠습니다.
다만 이러면 Home 페이지에서도 lazy를 적용하니까 첫 페이지(Home)에 들어갔을 때, 로딩 시간이 걸린다는 단점이 있을 수 있겠네요😭
로딩 시간이 걸린다는 단점이 있을 것이라고는 생각하지 않습니다. 사용자가 제일 처음 memegle 사이트에 방문하게 되면 Home 페이지를 그리기 위한 리소스들을 다운로드 받을 것이고, 그 시간 동안 fallback UI를 보여주게 됩니다. 네트워크 환경에 따라서 로딩 시간의 차이는 있겠지만 lazy를 적용하더라도 바로 리소스들을 다운로드 받을 것이라고 예상되어 lazy를 적용하기 때문에 로딩 시간이 더 걸릴 것이라고 생각하지는 않아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로딩 시간이 걸린다는 단점이 있을 것이라고는 생각하지 않습니다. 사용자가 제일 처음 memegle 사이트에 방문하게 되면 Home 페이지를 그리기 위한 리소스들을 다운로드 받을 것이고, 그 시간 동안 fallback UI를 보여주게 됩니다. 네트워크 환경에 따라서 로딩 시간의 차이는 있겠지만 lazy를 적용하더라도 바로 리소스들을 다운로드 받을 것이라고 예상되어 lazy를 적용하기 때문에 로딩 시간이 더 걸릴 것이라고 생각하지는 않아요!
흠....
저는 단순하게 사람들이 대부분 Home 페이지로 접근을 하니까 Home에도 lazy가 걸려 있으면 초기 로딩 지연이 존재할 것 같았어요. 그래서 사용자 입장에서 좋진 않을 것 같다?고 생각해서 Home에 lazy를 주지 않는 방식도 생각했어요. 그런데 그렇게 하게 되면 Home 페이지 js 파일 분리가 안 되겠네요...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Home에 lazy를 주지 않는 방식도 생각했어요
저도 이 방법을 생각했지만, 서비스에 익숙한 사용자라면 바로 url을 통해서 Search 페이지에 접근할 수도 있다고 생각했어요. (제가 딱 그렇거든요,,,자주 쓰는 서비스면 그냥 url을 외우고 입력해서 바로 들어갑니다.) 그렇다면 사용자는 Search 페이지에 있지만 Home 페이지를 그리기 위한 리소스도 함께 다운받게 되겠죠. Home 페이지에 dynamic import
를 적용하는 것은 결국, trade off인 것 같습니다. 쑤쑤만의 의견을 정해보세요 :)
src/pages/Home/Home.tsx
Outdated
import heroImage from '../../assets/images/hero.png?as=webp'; | ||
import trendingGif from '../../assets/images/trending.gif?as=webp'; | ||
import findGif from '../../assets/images/find.gif?as=webp'; | ||
import freeGif from '../../assets/images/free.gif?as=webp'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
png, gif 모두 webp로 변환하셨는데 최신 브라우저에서는 webp를 모두 지원하지만 사파리 옛날 버전과 같이 webp를 지원하지 않는 경우가 은근히 있더라고요. picture
태그 관련 내용도 참고해 보시면 좋을 것 같아요!
https://caniuse.com/?search=webp

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gif를 webm으로 변환할 지 고민하셨다고 했는데 최종적으로 webp로 변경하신 이유가 있으실까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.


오호.. 쑤쑤 덕분에 잊고 살았던 Can i Use 웹 사이트를 다시 방문해서 jpeg, webp 확장자를 지원하는 브라우저 버젼을 비교해 보는 시간을 가졌습니다. Safari, FrieFox를 제외하면 webp가 더 많은 브라우저 버젼을 지원해 주는 것으로 확인되는데요. 그래도 훨씬 더 많은 브라우저 버젼을 지원해 주기 위해서 jpeg 확장자도 포함을 해야겠네요!
왜 png가 아닌 jepg를 사용하는지 추가로 궁금해하실 수도 있을 것 같은데요.

위 내용들을 참고해서, 이미지 최적화에는 Jpeg를 추천하고, 손실 압축을 사용하기 때문에 크기를 더 작게 만들 수 있고, 투명도가 필요한 이미지가 아니기 때문에 Jpeg를 사용하기로 했어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gif를 webm으로 변환할 지 고민하셨다고 했는데 최종적으로 webp로 변경하신 이유가 있으실까요?
어떤 파일 확장자를 사용할지에 대해서 정말 많은 고민을 했었는데요. gif도 webp로 변환할 수가 있고, 용량도 줄여주기 때문에 사용했었습니다. webpack 파일에 설정을 해두고 png, gif를 webp로 한 번에 변환하는 편함도 느끼기 위한것도 의사 결졍 요소 중 하나였습니다.
쑤쑤의 피드백을 받은 후 다시 한 번 리소스 파일 확장자에 대해서 고민을 했었는데, 크기를 최적화 한 gif를 사용하는 것으로 변경하게 되었습니다. 그 이유는 webp, webm, m4 모두 파일의 크기를 줄여주고 video
태그를 사용할 수 있도록 해준다는 장점은 있겠으나(mp4의 경우) 검색 결과로 나오는 리소스들은 모두 gif이고 서비스도 gif를 검색하고 보여주는 서비스이기 때문에 리소스 통일성을 위해서 gif를 사용하는 것이 나을 것 같다고 판단했어요.
webp, webm의 경우 브라우저 버젼에 따라 지원할 수 있는 범위에 한계가 있기도 하고, mp4를 사용하는 경우에는 Content-Type이 이미지에서 미디어로 변경되기 때문에 마음에 드는 움짤을 다운받으려고 하는 사용자가 당황할 수도 있겠다는 생각을 하기도 했습니다. 그래서 크기를 최적화 한 gif를 사용하는 것으로 변경하기로 했어요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
검색 결과로 나오는 리소스들은 모두 gif이고 서비스도 gif를 검색하고 보여주는 서비스이기 때문에 리소스 통일성을 위해서 gif를 사용하는 것이 나을 것 같다고 판단했어요. mp4를 사용하는 경우에는 Content-Type이 이미지에서 미디어로 변경되기 때문에 마음에 드는 움짤을 다운받으려고 하는 사용자가 당황할 수도 있겠다는 생각을 하기도 했습니다.
이 부분이 상당히 납득이 되네요👍
생각해보면 서비스 자체가 gif 중심이기 때문에, 사용자는 당연히 gif라고 생각하고 다운로드 받았다가 갑자기 mp4 동영상 파일이 다운로드되면 당황스럽겠네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
바로 그거에요 쑤코톡!
@@ -44,7 +44,7 @@ const Home = () => { | |||
</Link> | |||
</div> | |||
</section> | |||
<CustomCursor text="memegle" /> | |||
<CustomCursor text="쑤쑤나는해리야" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/pages/Home/Home.tsx
Outdated
<h1 className={styles.title}>Memegle</h1> | ||
<h1 className={styles.title}>memegle</h1> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오잉?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cloudfront 캐싱을 적용한 후 배포 파일이 변경되었을 때, 브라우저가 새로운 js 파일을 받아오는지 테스트 하기 위해서 약간의 변화를 줬었는데 다시 수정한다는 것을 깜빡했네요 ㅎ
opacity: 0; | ||
transition: 0.5s all cubic-bezier(0.82, 0.085, 0.395, 0.895); | ||
will-change: transform, opacity; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오! will-change
속성도 있군요!
하나 배워갑니다 👍 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네넹 해당 속성을 사용하면 브라우저가 will-changle
에 명시된 스타일을 처리하기 위한 새로운 레이어를 하나 추가한다고 하네요!
위 벨로그 글에 will-change
속성을 사용하면 어떻게 되는지 시각화가 잘 되어 있어 공유해 봅니다!
"prettier": "^2.3.2", | ||
"sharp": "0.33.5", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오?? 어디에 쓰이는 친구인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 이미지 파일 확장자나 크기를 직접 변경하는 방법을 선택하지 않고, 웹팩에서 제공해 주는 플러그인을 활용했습니다.
해당 문서에서 제시하는 방법 중, sharp를 사용하게 되었습니다. image-minimizer-webpack-plugin
만 설치하면 이미지 최적화를 위한 모든 기능을 사용할 수 있게 되는 줄 알았지만, 추가로 sharp 의존성이 필요했습니다.
optimization: {
minimizer: [
`...`,
new CssMinimizerPlugin(),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
webp: {
quality: 80
}
}
}
},
generator: [
{
preset: 'webp',
implementation: ImageMinimizerPlugin.sharpGenerate,
options: {
encodeOptions: {
webp: {
quality: 80,
lossless: false
}
}
}
}
]
})
],
그래서 설치하게 되었습니다.
getTrendingWithCache: async () => { | ||
return await apiClientWithCache({ | ||
queryKey: 'trending-gifs', | ||
staleTime: 1000 * 60 * 5, | ||
queryFn: gifAPIService.getTrending | ||
}); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
세션 스토리지, 캐시 스토리지 사용법에 익숙하지 않기도 했고 캐싱 역할을 가지는 모듈을 직접 구현해 보고 싶기도 해서 Cache
클래스를 직접 만드는 방법을 선택했습니다!
@@ -14,8 +14,7 @@ const CustomCursor = ({ text = '' }: CustomCursorProps) => { | |||
|
|||
useEffect(() => { | |||
if (cursorRef.current) { | |||
cursorRef.current.style.top = `${mousePosition.pageY}px`; | |||
cursorRef.current.style.left = `${mousePosition.pageX}px`; | |||
cursorRef.current.style.transform = `translate(${mousePosition.pageX}px, ${mousePosition.pageY}px)`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쑤쑤의 질문 타임~
Q. translate 속성이 왜 Layout Shift를 일으키지 않을까요?
단순히 reflow를 일으키지 않아서. 안됩니다. 코린이도 이해하기 쉽게 설명해주세요^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하하 꼭 제 조카가 물어보는 것 같군요^^
코린이도 이해할 수 있도록 정말 low level에서 설명해 보자면,
- 리플로우가 발생하는 곳 : 부산
- 리플로우가 발생하지 않는 곳 : 서울
translate는 서울 주민이기 때문입니다.
translate 속성을 사용하면 브라우저는 translate로 결정되는 요소들의 위치, 크기를 reflow가 발생하는 레이어(부산)이 아닌 새로운 레이어(서울)에서 처리하게 됩니다. 그래서 reflow가 발생하지 않게 됩니다. traslate 속성은 CPU가 아닌 GPU를 사용하게 되며 GPU는 브라우저의 메인 스레드와 별개로 동작하기 때문에 성능에도 좋다고 합니다.
제 설명이 부족했을수도 있지만, z-index의 동작 방식을 완전히 파악하고 있는 쑤쑤라면 제 설명을 충분히 이해할 수 있을 것이라고 믿어요!!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10점 만점의 8.7점 드리겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1.3점이 깎인 부분을 설명해 주세요
import { useEffect, useRef } from 'react'; | ||
|
||
type ScrollHandler = () => void; | ||
|
||
const useScrollEvent = (onScroll: ScrollHandler) => { | ||
const requestAnimationFrameRef = useRef<number | null>(null); | ||
|
||
useEffect(() => { | ||
const handleScroll = (event: Event) => { | ||
onScroll(); | ||
const handleScroll = () => { | ||
if (requestAnimationFrameRef.current) { | ||
cancelAnimationFrame(requestAnimationFrameRef.current); | ||
return; | ||
} | ||
|
||
requestAnimationFrameRef.current = requestAnimationFrame(() => { | ||
onScroll(); | ||
requestAnimationFrameRef.current = null; | ||
}); | ||
}; | ||
|
||
window.addEventListener('scroll', handleScroll); | ||
|
||
return () => { | ||
window.removeEventListener('scroll', handleScroll); | ||
|
||
if (requestAnimationFrameRef.current) { | ||
cancelAnimationFrame(requestAnimationFrameRef.current); | ||
} | ||
}; | ||
}, [onScroll]); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partially Presented Frame을 최소로 발생시키기 위해 requestAnimationFrame을 활용하셨군요. 해결하지 못해서 고민하고 있었는데 한번 사용해 봐야겠네요👍 다만 크게 개선되지 않는 것 같아서 고민이네요....😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요구사항 자체가 Partially Presented Frame 현상을 최소화하는 것이라, 아예 없애는 것은 불가능할 것 같다고 판단해서 Request Animation Frame API를 활용해서 최소화해 보는 것으로 만족했습니다!
저는 공식문서를 이렇게 읽는 것 같습니다. 추상적인 문장을 잘 이해를 하지 못하는 편이라 무조건 예시부터 보는 것 같아요. 예시가 없다면 그림을 찾습니다. 리액트 공식문서를 읽는다고 해봅시다. 리액트 공식문서 - 탈출구를 읽으면 저는 우선 탈출구부터 이해를 못합니다. 그래서 바로 예시를 찾으러 내려가는 것 같아요. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요! 해리!
저의 리뷰가 마음에 드셨을랑가 모르겠네요~^,^
인상 깊었던 포인트가 피드백을 무조건 반영하는 것이 아닌 명확한 근거를 대고 반영하시는 모습이 좋았어요! 리뷰하면서 저도 공부한 것 같은 느낌이...들었어요 😅 그리고 공식 문서 읽는 꿀팁! 감사해요! 앞으로 공식 문서와 친해져 볼게요~~
추가 질문들에 대한 답변 부탁드릴게요~
<link | ||
href="https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" | ||
rel="preload" | ||
as="style" | ||
/> | ||
<link | ||
rel="preload" | ||
href="https://fonts.gstatic.com/s/josefinsans/v32/Qw3aZQNVED7rKGKxtqIqX5EUDXx4Vn8sig.woff2" | ||
as="font" | ||
type="font/woff2" | ||
crossorigin | ||
/> | ||
<link | ||
rel="preload" | ||
href="https://fonts.gstatic.com/s/josefinsans/v32/Qw3EZQNVED7rKGKxtqIqX5EUCEx6XHgOiJM6.woff2" | ||
as="font" | ||
type="font/woff2" | ||
crossorigin | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 네트워크탭 활용 멋지네요~ 네 맞습니다! 쑤쑤가 보내준 사진처럼 우선적으로 불러오는 것입니다!
"sideEffects": [ | ||
"*.css" | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 친구를 추가해서 어떤 변화가 있는 건가요??
sideEffects에 대해서 찾아보니까 tree shaking 관련한 코드 같은데, 제거해도 별다른 차이가 보이진 않는 것 같아서요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sideEffects
는 웹팩에게 tree shaking에서 export 하지만 import 하지 않는 코드들을 제거해도 된다고 명시하는 속성입니다. 저는 미션을 진행할 때,
"sideEffects": false,
위와 같이 설정하니 스타일이 깨지는 문제가 있어 css 파일은 tree shaking이 적용되지 않도록 했어요!
const App = () => { | ||
return ( | ||
<Router basename={'/perf-basecamp'}> | ||
<Router> | ||
<NavBar /> | ||
<Routes> | ||
<Route path="/" element={<Home />} /> | ||
<Route path="/search" element={<Search />} /> | ||
</Routes> | ||
<Suspense | ||
fallback={<div style={{ width: '100vw', height: '100vh' }}>loading...please wait...</div>} | ||
> | ||
<Routes> | ||
<Route path="/" element={<Home />} /> | ||
<Route path="/search" element={<Search />} /> | ||
</Routes> | ||
</Suspense> | ||
<Footer /> | ||
</Router> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍👍👍💯💯💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
혹시...유조?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
미션하느라 고생 많으셨어요 해리!
해리의 코드를 리뷰할 수 있어서 영광이었습니다! 수고하셨습니다👍
🔥 결과
CloutFront 접근 경로
개선 전 성능 측정
개선 후 성능 측정
프랑스 파리 Fast 3G 환경에서 방문한 결과
✅ 개선 작업 목록
1. 요청 크기 줄이기
Home 페이지에서 불러오는 스크립트 리소스 크기 < 60kb ✅
스크립트 리소스 크기를 모두 더해보면
59.8kb
인 것을 확인할 수 있습니다. 정말 아슬아슬하게 통과했네요...!소스코드 크기를 줄이기 위한 여정
자바스크립트의 리소스 크기를 개선하기 위한 계획들을 세웠으나, 웹팩 4 버젼 이상부터는 프로덕션 빌드를 할 때, 자동으로 코드를 최소화하기 위한 플러그인을 포함한다고 합니다. 그래서 따로 자바스크립트 리소스 크기 개선을 위한 작업을 진행하지는 않았습니다. (참고)
css-in-js 라이브러리를 쓰거나, style-loader를 사용하게 되면 번들링 될 때, 자바스크립트 파일에 css 코드들이 포함됩니다. 그래서 자바스크립트 리소스 크기가 커집니다. 저는 여기서 의문을 좀 가졌었어요.
라는 의문을 가졌였습니다. 하지만 웹팩 공식문서에서도 확인할 수 있듯 프로덕션 빌드를 할 때는 js파일에서 css파일을 추출할 것을 권장하고 있더군요.
위 글을 읽고나서 추가로, 브라우저의 렌더링 엔진은 싱글 스레드로 동작하는데 어떻게 병렬 로딩이 가능한 것인가? 라는 추가적인 의문을 가졌었어요. 해당 글을 읽은 후, 브라우저는 여러 개의 네트워크 연결을 동시에 열 수 있고 이를 통해서 여러 리소스를 병렬로 다운로드 할 수 있다는 내용을 알게되었습니다.
분리의 이점에 대해서 이해를 한 후,
두 플러그인을 활용해서 css 리소스의 크기 자체를 줄이고, 번들링된 js 파일에서 css를 분리하는 작업을 진행했습니다.
webpack-bundle-anaylzer 도구를 활용해서, 확인해 본 결과 두 플러그인을 사용한 후 번들링 된 js 파일에서 css가 사라진 것을 확인할 수 있었어요.
120kb
이하이미지 크기를 개선하기 위해서 �웹팩에서 제공해 주는 이미지 크기를 줄여주는 플러그인을 활용했습니다.
hero.png
, 와 3가지 webp모두webp
확장자를 사용하도록 했습니다. gif의 경우 webm으로 변경할 지에 대한 고민을 했었는데, webp 또한 영상(gif)을 지원하기 때문에, 플러그인 설정을 통해서 모두 webp로 변경하는 방법을 적용했습니다.2. 필요한 것만 요청하기
Home 페이지에서 불러오는 스크립트 리소스에 Search 페이지의 소스 코드가 포함되지 않아야 한다. ✅
페이지별 리소스를 분리하기 위한 여정
웹팩 설정의
optimization
중splitChuncks
를 활용해서 중복되는 자바스크립트 코드 덩어리들을 하나의 chuncks로 분리할 수 있도록 했어요. (관련 커밋)사용자가 현재 방문한 페이지를 그리기 위한 리소스만 다운 받을 수 있도록 하기 위해서
dynamic import
,React Suspense
를 활용했습니다. (관련 커밋)react-icons 패키지에서 실제로 사용하는 아이콘 리소스만 빌드 결과에 포함되어야 한다. ✅
실제로 사용하는 리액트 아이콘만 사용하기 위해서 번들링할 때, 사용하지 않는 아이콘들을 포함하지 않는 트리 쉐이킹을 진행해야 했습니다. 해당 내용을 학습하면서 프로덕션 빌드를 할 때는 트리 쉐이킹 과정을 포함해준 다는 것을 알게되어 따로 라이브러리를 설치하거나 아이콘 패키지리를 트리 쉐이킹하기 위한 추가 작업을 진행하지 않았습니다.
확인해 보면 사용하지 않는 아이콘은 포함되지 않아 번들 사이즈가 줄어든 것을 확인할 수 있어요.
3. 같은 건 매번 새로 요청하지 않기
aws에서 제공하는 S3에 최적화된 캐싱 정책을 사용했고, 모모 FE 팀들과 논의한 결과로 결정된
max-age=1년
을 적용하기로 했습니다. 설정할 수 있는 응답 헤더 정책의 갯수를 초과해서 저희와 같은 기간으로 설정한 투룻의 헤더 정책을 빌려서 사용했어요. 1년으로 적용한 이유는 기간과 비례해서 캐시 적중률이 높아질 수 있고, 웹팩 설정 파일에서contenthash
를 사용해 파일 내용이 변경되면 파일 이름 hash 값도 변경되도록 하여 사용자가 항상 최신 버젼을 바라볼 수 있기 때문에 기간을 가장 길게 하는 것이 좋을 것 같다고 판단했습니다.5분 정도면 트렌딩 결과가 변경될 수도 있을 것 같아, 5분 동안 trending API 결과를 캐싱하기 위한 캐시 모듈을 구현했습니다. (관련 커밋)
4. 최소한의 변경만 일으키기
리액트에서 제공하는
memo
함수를 사용해서 이전 props와 동일하면 memoization한 렌더링 결과를 재사용하도록 했어요. (관련 커밋)프로파일러로 확인해 보면, 리렌더되지 않는 것을 확인할 수 있습니다.
CustomCursor, 검색 결과 > hover, 도움말 패널 열고닫기 애니메이션에서 모두
layouf shift
현상이 발견되었습니다. 해당 현상을 해결하기 위해서 reflow를 발생시키지 않는 css 속성을 사용했어요. (관련 커밋)Frame Drop 현상은 발생하지 않았지만, Partially Presented Frame 현상을 최소로 발생시키기 위해서 스크롤 이벤트에
requestAnimationFrame
API를 적용했습니다. (관련 커밋)적용 후, 다시 측정해 보니 크게 개선되지는 않았던 것 같아요.
Partially Presented Frame 현상이 발생한 폭(?)은 줄어든 것 같네요.
🧐 공유
쑤쑤의 웹 성능 최적화를 위한 더 깊은 학습을 위해서 제가 많이 참고했던 좋은 자료들을 공유드릴게요~~