-
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
[개인 미션 - 성능 오답노트] 빙봉(김윤경) 미션 제출합니다. #143
Conversation
- 배포 URL을 사용자 계정에 맞게 수정 - .gitignore 파일에 .DS_Store 추가
CSS 파일 최적화를 위해 CssMinimizerWebpackPlugin과 MiniCssExtractPlugin을 적용하여 파일 크기 감소 및 로딩 속도 개선
- ImageMinimizerPlugin을 사용하여 이미지 압축 구현 - WebP 포맷으로 이미지 변환 기능 추가 - 이미지 품질 및 압축 옵션 설정
- splitChunks를 적용하여 chunk를 분리 - 파일 이름에 컨텐츠 해시를 포함하여 브라우저 캐싱을 효과적으로 사용하도록 설정
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 API를 이용해 구현해 주신 로직이 간결하면서도 명료해서 좋았어요. 저도 이런 코드를 써야 하는데... 😢
구현해주신 내용 관련하여 몇 가지 궁금한 사항과, 한 가지 개선점이 보여서 코멘트를 남겨드렸습니다.
아울러 코멘트에 포함되지 않은 자잘한 이슈도 함께 공유드릴게요. 아래 내용은 여유 되실때 따로 확인해보시면 될 것 같습니다.
- 배포 경로의
/search
로 바로 접근할 경우 403 forbidden 에러 발생- AWS 콘솔에서 CloudFront 배포 정보 -> 에러 페이지를 확인해 보세요.
- 참고 링크 : React 프로젝트를 CloudFront 로 배포 시 403/404 에러 발생
FeatureItem
컴포넌트에서<img>
요소에alt
속성 부재
@@ -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 = `translate3d(${mousePosition.pageX}px, ${mousePosition.pageY}px, 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.
혹시 translate(x, y)
와 translate3d(x, y, z)
의 성능 차이를 비교해 보셨을까요? 다른 PR들을 살펴보니 이 부분은 크루마다 다른 선택을 한 것으로 보이는데, translate3d
를 선택하신 이유가 궁금해요.
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.
초기 의도
translate3d
는 하드웨어 가속, 즉 GPU 가속을 적극적으로 활용한다고 알고 있었기 때문에, 더 부드러운 움직임을 기대하며 translate3d
를 사용했습니다.
성능 테스트 결과
아르의 코멘트를 받고 translate3d
와 translate
를 각각 사용해 성능 테스트를 해보았는데, 차이가 거의 없더라고요🤣
또한, translate3d(x, y, 0)
처럼 z 축이 0인 경우, 브라우저는 이를 2D 변환처럼 처리하기 때문에, 제가 작성한 코드에서는 사실상 translate
와 비슷한 효과였던 것 같습니다.
결론적으로, translate3d
는 과유불급이었던 것 같아서 translate로 변경했습니다 :) 좋은 질문 감사해요
translate3d
는 더 복잡한 3D 애니메이션이나 z축을 포함한 입체적인 움직임을 구현할 때 더 적합할 것 같아요!
반영 커밋: 30bce7a
const fetchTrending = async () => { | ||
if (status !== SEARCH_STATUS.BEFORE_SEARCH) return; | ||
|
||
try { | ||
const cachedTrending = await getCacheItem<{ | ||
data: GifImageModel[]; | ||
timestamp: number; | ||
}>(TRENDING_CACHE_KEY); | ||
|
||
if (cachedTrending && !isCacheExpired(cachedTrending.timestamp, TRENDING_CACHE_EXPIRY)) { | ||
setGifList(cachedTrending.data); | ||
return; | ||
} | ||
}; | ||
|
||
const gifs = await gifAPIService.getTrending(); | ||
setGifList(gifs); | ||
|
||
await setCacheItem(TRENDING_CACHE_KEY, { data: gifs, timestamp: Date.now() }); | ||
} catch (error) { | ||
handleError(error); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
fetchTrending(); | ||
}, []); |
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.
리팩토링을 하시면서 fetchTrending
함수를 useEffect 바깥으로 옮기셨군요! 현재는 Search
페이지에서만 최초 1회에 한해서 fetch를 진행하므로 별다른 사이드 이펙트가 발생하지 않고 있지만, 만약 해당 페이지가 모종의 이유로 리렌더링이 일어난다면 fetchTrending
함수가 재생성되면서 불필요한 메모리 할당이 일어날 거에요. 이 함수 내부에서 사용되는 상태값이나 함수에 대한 의존성 관리도 어려워지구요.
따라서 fetchTrending
함수 코드는 useEffect 내부에 위치하는게 좋을 것 같아요.
관련하여 참고하실 만한 링크를 첨부해 드립니다.
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.
오호👍 useEffect
내부에 함수를 정의하는 이유를 정확히 몰랐었는데, 코멘트 덕분에 그 중요성을 확실히 알게 되었습니다. 메모리 관리와 의존성 관리 측면에서 훨씬 효율적이라는 점을 배웠어요! 좋은 리뷰 감사합니다, 아르!!
반영 커밋: cbdc548
src/utils/cache.ts
Outdated
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 API를 활용한 로컬 캐싱 로직을 아주 깔끔하게 만들어 주신 것 같아요. 🥇
encodeOptions: { | ||
webp: {}, | ||
png: {}, | ||
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.
여기서 encodeOptions
아래에 각 포맷에 대해 빈 설정값({}
)을 넣을 경우 ImageMinimizerPlugin.sharpMinify가 어떻게 동작하게 되나요? 제가 이 플러그인에 익숙치 않아 궁금하여 여쭤봅니다. 🙏
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.
감사해요. 덕분에 유용한 정보를 알아갑니다 🙏
import { GifImageModel } from '../../../../models/image/gifImage'; | ||
|
||
import styles from './GifItem.module.css'; | ||
|
||
type GifItemProps = Omit<GifImageModel, 'id'>; | ||
|
||
const GifItem = ({ imageUrl = '', title = '' }: GifItemProps) => { | ||
const GifItem = memo(({ imageUrl = '', title = '' }: GifItemProps) => { |
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.memo를 깔끔하게 적용해 주셨네요 👍
|
||
module.exports = { | ||
entry: './src/index.tsx', | ||
resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }, | ||
output: { | ||
filename: 'bundle.js', | ||
filename: '[name].[contenthash].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.
번들링 된 파일명에 contenthash
를 추가하신 부분 좋네요! 👍
{ | ||
// You can apply generator using `?as=webp`, you can use any name and provide more options | ||
preset: 'webp', | ||
implementation: ImageMinimizerPlugin.sharpGenerate, | ||
options: { | ||
encodeOptions: { | ||
webp: { | ||
quality: 80, | ||
lossless: false | ||
} | ||
} | ||
} | ||
} |
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.
기존 디자인 리소스에 대해 webp
변환을 일괄적으로 설정해 주셨는데요. png
와 gif
의 변환 대상으로 모두 webp
를 선택하신 이유가 있으실까요? 특히 animated gif의 경우 webm
, avif
, mp4
등 대체 가능한 다른 포맷들도 존재하는데, 이들 대신 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
는 webp
와 avif
중 고민했습니다. (이미지 포맷 중에서 더 높은 압축률을 제공하는 webp
와 avif
를 최종 후보로 고민했습니다!)
avif
가 webp
보다 더 높은 압축 성능을 제공하지만, 브라우저 호환성이 아직 부족합니다. 또한, 최근에 해외에서는 넷플릭스와 같은 플랫폼이 avif
를 지원하기 시작했지만, 한국 웹사이트에서는 여전히 많이 사용되지 않는 것으로 확인되었습니다. 따라서, 압축률은 avif
보다 다소 낮지만 브라우저 호환성이 더 좋은 webp
를 선택했습니다!
gif의 경우에는 webp
, mp4
, webm
을 고려했습니다. mp4
가 가장 높은 압축률을 제공하지만, mp4
와 webm
은 비디오 포맷이기 때문에 선택하지 않았습니다. gif
는 애니매이션을 지원하는 이미지 포맷이므로, 이미지 포맷인 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
)의 경우 webp
포맷으로,
애니메이션 이미지(gif
)의 경우는 H.264 코덱 기반 mp4
포맷으로 변경했어요.
png
-> webp
변환 이유
정적 이미지의 webp
변환은 빙봉과 거의 같은 고민의 과정을 거쳐 선택했어요. 높은 압축률과 지원 환경의 보편성을 함께 얻을 수 있는 좋은 방법이라 생각했습니다. avif
가 시장에 정착되기까지는 아직 시간이 더 필요할 것 같아요.
gif
-> mp4
변환 이유
gif
를 mp4
로 변환한 이유는, 해당 파일들이 지닌 1초+ 분량의 화질+움직임을 매끄럽게 유지하면서 용량을 줄일 수 있는 가장 효율적인 방법이라고 생각했기 때문이에요.
animated gif와 같은 이미지 기반 '움짤'은 기본적으로 image sequencing 기술에 의존하고 있습니다. 즉, 정적 이미지들이 프레임 단위로 연속 배치된 형태입니다. 예를 들어 초당 15프레임짜리 1초 분량의 animated gif 안에는 15장의 GIF 이미지와 이들의 sequencing 정보가 함께 담겨있다고 보시면 되어요.
동영상 압축은 조금 다르게 진행됩니다. 예를 들어, 어느 한 순간에 존재하는 하나의 키프레임(keyframe)에 대해 완전한 이미지 형태의 정보를 담아두되, 그 전후의 몇 프레임에는 해당 키프레임과 시각적으로 차이가 나는 정보만 담는 방식으로 압축합니다. 일종의 눈속임 효과를 이용하는 셈이에요. 그래서 이미지 기반 포맷보다 대부분 압축 효율이 높습니다. 음성 정보를 제거하고 인코딩하면 용량이 더욱 줄어들고요.
이미지냐 영상이냐에 대한 구분보다는... 이것이 브라우저에서 범용적으로 지원되는 포맷인가, 기존의 사용자 경험을 해치지 않으면서 화질 대비 용량 효율을 높일 수 있는가를 기준으로 삼다 보니 이렇게 결정하게 되었네요 😅
참고로 webm
이 아닌 H.264 코덱 기반 mp4
를 택한 이유는 브라우저 지원 범위가 가장 넓기 때문입니다. (심지어 IE 9에서도 재생되어요)
혹시 이 부분에 관심이 있으시다면 아래 글들을 참고하시면 좋을 것 같습니다.
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.
@@ -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 = `translate3d(${mousePosition.pageX}px, ${mousePosition.pageY}px, 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.
초기 의도
translate3d
는 하드웨어 가속, 즉 GPU 가속을 적극적으로 활용한다고 알고 있었기 때문에, 더 부드러운 움직임을 기대하며 translate3d
를 사용했습니다.
성능 테스트 결과
아르의 코멘트를 받고 translate3d
와 translate
를 각각 사용해 성능 테스트를 해보았는데, 차이가 거의 없더라고요🤣
또한, translate3d(x, y, 0)
처럼 z 축이 0인 경우, 브라우저는 이를 2D 변환처럼 처리하기 때문에, 제가 작성한 코드에서는 사실상 translate
와 비슷한 효과였던 것 같습니다.
결론적으로, translate3d
는 과유불급이었던 것 같아서 translate로 변경했습니다 :) 좋은 질문 감사해요
translate3d
는 더 복잡한 3D 애니메이션이나 z축을 포함한 입체적인 움직임을 구현할 때 더 적합할 것 같아요!
반영 커밋: 30bce7a
const fetchTrending = async () => { | ||
if (status !== SEARCH_STATUS.BEFORE_SEARCH) return; | ||
|
||
try { | ||
const cachedTrending = await getCacheItem<{ | ||
data: GifImageModel[]; | ||
timestamp: number; | ||
}>(TRENDING_CACHE_KEY); | ||
|
||
if (cachedTrending && !isCacheExpired(cachedTrending.timestamp, TRENDING_CACHE_EXPIRY)) { | ||
setGifList(cachedTrending.data); | ||
return; | ||
} | ||
}; | ||
|
||
const gifs = await gifAPIService.getTrending(); | ||
setGifList(gifs); | ||
|
||
await setCacheItem(TRENDING_CACHE_KEY, { data: gifs, timestamp: Date.now() }); | ||
} catch (error) { | ||
handleError(error); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
fetchTrending(); | ||
}, []); |
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.
오호👍 useEffect
내부에 함수를 정의하는 이유를 정확히 몰랐었는데, 코멘트 덕분에 그 중요성을 확실히 알게 되었습니다. 메모리 관리와 의존성 관리 측면에서 훨씬 효율적이라는 점을 배웠어요! 좋은 리뷰 감사합니다, 아르!!
반영 커밋: cbdc548
encodeOptions: { | ||
webp: {}, | ||
png: {}, | ||
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.
{ | ||
// You can apply generator using `?as=webp`, you can use any name and provide more options | ||
preset: 'webp', | ||
implementation: ImageMinimizerPlugin.sharpGenerate, | ||
options: { | ||
encodeOptions: { | ||
webp: { | ||
quality: 80, | ||
lossless: false | ||
} | ||
} | ||
} | ||
} |
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
는 webp
와 avif
중 고민했습니다. (이미지 포맷 중에서 더 높은 압축률을 제공하는 webp
와 avif
를 최종 후보로 고민했습니다!)
avif
가 webp
보다 더 높은 압축 성능을 제공하지만, 브라우저 호환성이 아직 부족합니다. 또한, 최근에 해외에서는 넷플릭스와 같은 플랫폼이 avif
를 지원하기 시작했지만, 한국 웹사이트에서는 여전히 많이 사용되지 않는 것으로 확인되었습니다. 따라서, 압축률은 avif
보다 다소 낮지만 브라우저 호환성이 더 좋은 webp
를 선택했습니다!
gif의 경우에는 webp
, mp4
, webm
을 고려했습니다. mp4
가 가장 높은 압축률을 제공하지만, mp4
와 webm
은 비디오 포맷이기 때문에 선택하지 않았습니다. gif
는 애니매이션을 지원하는 이미지 포맷이므로, 이미지 포맷인 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.
빙봉 고생 많으셨어요!
연휴 앞두고 팀 프로젝트와 미션을 병행하시느라 힘드셨을텐데, 꼼꼼히 체크하시고 답변도 남겨주셔서 감사드립니다.
이미지/영상 포맷 관련 질문 주신 내용에는 별도로 코멘트를 남겨드렸어요. 시간 여유 되실때 찬찬히 살펴보시면 좋을 것 같습니다.
앞으로 남은 미션, 프로젝트도 멋지게 해내시길 기원합니다. 화이팅이에요 🙏
아르 안녕하세요😊 잘 지내고 계신가요?? 미션 잘 부탁드립니다!
🔥 결과
🚀 lighthouse 측정 결과
🚀 webpagetest 측정 결과 (프랑스 파리에서 Fast 3G 환경으로 접속)
✅ 개선 작업 목록
1. 요청 크기 줄이기
JS 압축과 난독화
JS 코드를 최소화하고 난독화하기 위해서 Webpack의 TerserWebpackPlugin을 활용할 수 있습니다. 그러나 Webpack v5부터 코드 최적화와 압축을 위해 production 모드에서는 TerserWebpackPlugin이 자동으로 설정되기 때문에 별도로 설정하지 않았습니다.
CSS 압축
자바스크립트와 마찬가지로 CSS에도 해석에 불필요한 공백이 존재합니다. 이를 제거하기 위해서 Webpack의 CssMinimizerPlugin을 활용하였습니다. 추가적으로 CSS를 별도의 파일로 추출하기 위해서 MiniCssExtractPlugin을 사용하였습니다.
관련 커밋: 44b83d5
Webpack ImageMinimizerWebpackPlugin을 사용하여 압축 및 webp 변환(sharp 사용)하고, 수동 리사이징을 하였습니다.
2. 필요한 것만 요청하기
SplitChunksPlugin을 사용하여 공통으로 사용되는 코드를 별도의 파일로 분리하여 로드하였습니다. Webpack SplitChunks를 통해 bundle.js의 사이즈가
1.21 MB
에서1.16 MB
로 줄어들었습니다.React.lazy를 사용해 Home과 Search 컴포넌트를 동적으로 불러오고, Suspense를 이용해 로딩 중에 표시할 대체 텍스트(Loading...)를 보여주는 방식을 성능 개선하였습니다.
Webpack-bundle-analyzer를 사용해 번들 크기를 분석하면 Home과 Search 페이지의 소스 코드가 각각 분리된 것을 확인할 수 있었습니다.
Home 페이지에 접속하면 Home 페이지의 소스 코드인
601.16c1283ec74895a77004.js
만 로드되고, Search 페이지의 소스 코드인356.f674814de8d3de5a160d.js
는 로드되지 않은 것을 확인할 수 있었습니다.아이콘 패키지 Tree Shaking
Webpack 5 버전 이상부터는
mode: 'production'
으로 설정하면 기본적으로 Tree Shaking이 적용되기 때문에 별도의 설정을 하지 않았습니다. (ModuleConcatenationPlugin이 내부적으로 추가되어 동작하기 때문)production mode로 빌드된 번들 크기를 Webpack-bundle-analyzer를 사용해 분석하면 react-icons에서 Stat size가 634.84 KB, Parsed size가 2.68 KB, Gzipped size가 1.29 KB인 것을 확인할 수 있었습니다.
Parsed는 웹팩이 트리 쉐이킹을 적용한 후의 파일 크기입니다.
3. 같은 건 매번 새로 요청하지 않기
CloudFront 캐시 설정 / S3 메타데이터 설정 (설정값, 해당 값을 설정한 이유 포함)
캐싱 정책은 기본으로 지정된
CachingOptimized
정책을 사용했습니다.응답 헤더 정책을 추가하였습니다. Cache-Control 헤더를 추가하여 max-age를 최대값인
31536000
(=1년)을 적용하였습니다. 캐시 적중률이 변화가 적고 max age가 클수록 높아지기 때문입니다.GIPHY의 trending API를 Search 페이지에 들어올 때마다 새로 요청하지 않아야 한다.
Cache API를 사용하였습니다.
utils/cache.ts
파일에는 아래와 같은 함수들이 포함되어 있습니다.createCacheUtil
: 브라우저의 Cache API를 활용하여 데이터를 캐싱하고 만료 시간을 관리getCacheItem
: 캐시에서 특정 키로 데이터를 조회하는 함수setCacheItem
: 특정 키로 데이터를 저장하는 함수isCacheExpired
: 특정 데이터의 캐시가 만료되었는지 확인하는 함수로, 만료 시간이 지났는지 여부를 판단합니다.hooks/useGifSearch.tsx
에서cache.ts
를 활용하였습니다.만료 시간은 1시간으로 설정하였습니다. (TRENDING_CACHE_EXPIRY)
GIPHY의 Trending API 캐시
만료 시간
을1시간
으로 설정한 이유는 트렌드 데이터는 짧은 시간 내에 크게 변하지 않으면서도 최신 정보를 적당히 제공할 수 있기 때문입니다. 1시간 캐시를 사용하면 불필요한 API 호출을 줄여 네트워크 트래픽과 서버 부하를 감소시키며, 동시에 사용자에게 충분히 최신의 콘텐츠를 제공할 수 있다고 생각하였습니다.4. 최소한의 변경만 일으키기
검색 결과 > 추가 로드시 추가된 목록만 새로 렌더되어야 한다.
✈️ 관련 커밋: 45eb1cf
GifItem
컴포넌트가 load more 버튼을 눌렀을 때 기존에 렌더링된 항목들도 불필요하게 다시 렌더링되는 문제를 해결하기 위해,React.memo
를 적용하여 props가 동일할 경우 해당 컴포넌트가 다시 렌더링되지 않도록 최적화하였습니다.Layout Shift 없이 애니메이션이 일어나야 한다.
position
속성(top, bottom, left, right) 대신transform
을 활용하여 Layout Shift를 방지하였습니다.Chrome DevTools > Performance 탭에서
Layout Shift
발생 정도를 측정할 수 있습니다.Layout Shift
는 다음과 같은 상황에서 발생하였습니다.top
과left
대신transform
과translate
을 사용하여 레이아웃 변경을 방지하였습니다.hover했을 때
top: -0.75rem;
때문에 레이아웃 변경이 감지됩니다.top
대신transform
을 사용하여 레이아웃 변경을 방지하였습니다.(Chrome DevTools 기준) Partially Presented Frame 역시 최소로 발생해야 한다.
CPU: 6x 감속, 네트워크: 빠른 4G를 설정하였을 때 Frame Drop이 일어나지 않았습니다.
🧐 공유