안녕하세요. 휴먼스케이프 Hugh 입니다.
이번 시간에는 RN 공식 문서의 Performance 탭의 내용을 살펴보려고 합니다. 퍼포먼스 탭의 내용은 아래와 같이 구성됩니다.
Performance Overview
Optimizing Flatlist Configuration
Ram Bundles and Inline Requires
Profiling
오늘은 그 첫 번째 포스팅 Performance Overview 편 입니다. 포스팅 내용은 문서의 내용을 번역하고 읽기 편하도록 정리 및 요약을 한 것입니다.
Performance Overview
웹 뷰 베이스 대신 리액트 네이티브를 사용하는 설득력 있는 이유는 초당 60프레임 달성하고 네이티브 스럽게 보이고 느낄 수 있는 것입니다. 우리는 성능 최적화가 아닌, 앱을 만드는 것에 집중을 하도록 도와주고 싶지만 아직 부족한 부분이 있습니다. 리액트 네이티브가 최선의 방법을 결정할 수 없을 때 가 있고, 그렇기 때문에 개입이 필요합니다. 우리는 기본적으로 버터같이 부드러운 UI 퍼포먼스를 전달하려고 노력하지만, 때로는 불가능합니다.
이 가이드는 성능 이슈를 해결하기 위한 기본적인 것들을 전달하고 일반적인 원인과 해결방법을 논의하는 것입니다.
What you need to know about frames
우리 조부모 세대에는 영화를 “moving pictures” 이라고 불렀습니다. 그 이유는 비디오의 움직임이 정적 이미지를 빠르게 변경하여 생성된 환상이기때문입니다. 우리는 이러한 각 이미지를 프레임이라고 합니다. 초당 보여지는 프레임의 수는 얼마나 부드럽고 실제와 같이 보여 지는 지 직접적으로 영향을 끼칩니다. IOS 기기는 사용자가 보게 될 스크린의 정적 이미지(프레임)를 만들어 내는 모든 작업을 수행하는데 16.67ms의 시간을 줍니다. 만약 할당된 16.67ms의 시간동안 프레임을 만들어 내기 위한 모든 작업을 수행하지 못 할 경우 프레임은 삭제 되고, UI는 응답이 없는 것처럼 보여질 것입니다.
시뮬레이터 개발자 도구의 ‘Show Perf Monitor’ 메뉴를 클릭 해 보면 아래와 같이 두 개의 프레임이 있는 것을 확인할 수 있습니다.
시뮬레이터의 Show Perf Monitor 화면
JS frame rate (Javascript thread)
대부분의 RN 앱의 비즈니스 로직은 js 스레드에서 실행됩니다. 이 곳에 리액트 애플리케이션이 있고, API 호출이 이루어지고 터치 이벤트가 처리되는 곳입니다. native-backed 뷰에 대한 업데이트는 일괄 처리되어 프레임 생성에 주어진 시간 안에 이벤트 루프의 반복이 끝날 때 네이티브로 전송됩니다. js 스레드가 프레임에 대해 응답하지 않는 경우 드롭된 프레임으로 간주됩니다. 예를 들어, 복잡한 애플리케이션의 루트 컴포넌트에서 setState를 호출했고 그 결과로 트리의 하위 컴포넌트들의 값 비싼 리렌더링이 일어나는 경우 200ms 가 걸리고 12프레임이 드롭될 수 있습니다. js 스레드에 의해 컨트롤 되는 애니메이션의 경우 잠시동안 멈춘 것 처럼 보여질 수도 있습니다. 만약 어떤 부분에서 100ms 이상 걸린다면 유저는 그것을 느낄 수 있을 것 입니다.
이 것은 네비게이션 이동에서 자주 발생합니다. 새로운 경로를 푸시 할 때, js스레드는 네이티브 측으로 적절한 명령을 전달하기 위해 화면 구성에 필요한 모든 컴포넌트를 렌더링 해야 합니다. 화면 전환이 js 스레드에 의해 컨트롤 되기 때문에 몇 프레임을 잡아먹거나 버벅거리는 것은 일반적입니다. 또 componentDidMount에서 추가 작업을 수행할 경우 추가적인 버벅임이 발생할 수 있습니다.
또 다른 예시는 터치에 응답하는 것입니다. 만약 js 스레드에서 여러 프레임에 걸쳐 작업을 수행하는 경우, TouchableOpacity에 대한 반응은 지연될 것입니다. 네이티브 스레드에서 전송된 터치 이벤트를 js 스레드가 사용중이기 때문에 바로 처리할 수 없기 때문입니다.
UI frame rate (main thread)
많은 사람들은 NavigatorIOS의 성능이 Navigator 보다 뛰어나다는 걸 알고 있습니다. 그 이유는 전환에 대한 애니메이션이 전적으로 main 스레드에 의해 수행되기 때문에 js 스레드에서의 프레임 드랍에 의한 방해를 받지 않기 때문입니다.
비슷한 예로 js 스레드가 바쁠 때에도 우리는 ScrollView를 통해서 스크롤을 위 아래로 움직일 수 있습니다. 그 이유는 ScrollView가 main 스레드에 있기 때문인데요. 스크롤 이벤트는 js 스레드로 전송되지만 스크롤을 발생시키기 위해 이벤트를 전송하는 것은 아니기 때문입니다.
Common sources of performance problems
Running in development mode (dev=true)
js 스레드 성능은 dev 모드에서 급격하게 떨어집니다. 이 것은 개발자에게 좋은 경고와 에러메시지를 제공해주기 위해서 불가피 합니다. 그러니 최적화를 한 뒤 성능 테스트는 릴리즈 빌드에서 확인해야 합니다.
Using console.log statements
번들 앱을 실행할 때, 이러한 문의 경우 병목현상이 발생할 수 있습니다. redux-logger와 같은 디버깅 라이브러리들은 번들링할 때 삭제해 주어야 합니다. console.log를 제거해주는 플러그인을 사용할 수 도 있습니다. 플러그인을 설치하고 .bablerc 파일을 아래와 같이 수정해주면 됩니다.
npm i babel-plugin-transform-remove-console — save-dev
{ 'env': { 'production': { 'plugins': ['transform-remove-console'] } } }
ListView initial rendering is too slow or scroll performance is bad for large lists
FaltList 나 SectionList 를 사용합니다. API를 단순화 하는 것 외에 새로운 리스트 컴포넌트는 성능 향상이 되었습니다. 주요기능은 행 수에 관계없는 메모리 사용입니다.
만약 FlatList가 느리게 렌더링 된다면, 렌더된 아이템의 측정을 건너 뛰는 방법으로 렌더링 속도를 최적화 하도록 getItemLayout을 구현하였는지 확인해 보아야 합니다.
JS FPS plunges when re-rendering a view that hardly changes
ListView를 사용하는 경우 재 렌더링 여부를 빠르게 결정함으로써 많은 작업을 줄여주는 rowHasChanged function을 구현해야만 합니다. 만약 immutable 데이터 구조를 사용하는 경우 참조 동등성 검사만 필요합니다.(참고: ListView의 경우 DEPRECATED 된 컴포넌트 입니다.)
비슷한 예로, shouldComponentUpdate를 구현하여 컴포넌트 리렌더에 대해 정확하게 지시하여야 합니다. 만약 pure 컴포넌트(props와 state에 전적으로 의존하는 렌더링 함수를 반환하는)를 작성한다면, PureComponent를 사용하여 수행할 수 있습니다.
다시 한번 강조하자면 immutable한 데이터 구조는 이를 빠르게 유지하는데 유용합니다. 만약 큰 오브젝트 리스트를 깊게 비교해야 한다면 다시 렌더링 하는 것이 더 값이 적을 수 있습니다.
Dropping JS thread FPS because of doing a lot of work on the JavaScript thread at the same time
‘느린 네비게이터 전환’은 가장 일반적인 경우지만 다른 경우도 있습니다. InteractionManager의 사용이 좋은 접근이지만, 사용자 경험 비용이 너무 높아 애니메이션 작업을 연기시킬 수 없을 경우, LayoutAnimation을 고려할 수 있습니다.
Animated API는 useNativeDriver: true를 설정하지 않는한 각 키 프레임은 js 스레드에 의존합니다. 반면 LayoutAnimation은 Core Animation을 활용하여 js 스레드와 main 스레드 프레임 드롭의 영향을 받지 않습니다.
주의사항: LayoutAnimation은 fire-and-forget 애니메이션(정적 애니메이션)에서만 작동합니다. 중단 가능해야 하는 경우 Animated를 사용해야 합니다.
Moving a view on the screen (scrolling, translating, rotating) drops UI thread FPS
이것은 이미지 위에 투명한 배경이 있는 텍스트를 사용할 때, 또는 알파 합성이 필요한 상황에서 특히 그렇습니다. 이 경우 shouldRasterizeIOS, renderToHardwareTextureAndroid 를 사용하면 효과적입니다(View 컴포넌트의 옵션). 하지만 이러한 옵션들을 남용하게 되면 메모리가 부족할 수도 있습니다.
Animating the size of an image drops UI thread FPS
iOS의 경우 매 번 이미지 크기를 조절하는 건 매우 값 비싼 연산입니다. 이 경우 style 프로퍼티인 transform:[{scale}]을 사용해야 합니다.(예를 들어 이미지를 탭 하여 확대하는 기능)
My TouchableX view isn’t very responsive
터치로 인한 컴포넌트의 opacity나 highlight를 조절하는 일과 같은 프레임에서 어떤 액션이 하게 된다면 onPress 함수가 리턴될 때까지 기다려야 합니다. onPress 함수에서 setState를 수행하여 많은 작업을 수행하고 몇 개의 프레임이 삭제되면 위와 같은 문제가 발생할 수 있습니다. 이에 대한 해결책으로 requestAnimationFrame에서 onPress 핸들러 내부의 작업을 래핑하는 것입니다.
handleOnPress() { requestAnimationFrame(() => { this.doExpensiveAction(); }); }
Slow navigator transitions
네비게이터 애니메이션은 js스레드에 의해 제어됩니다. ‘push from right’ 스크린 전환이 일어났다고 가정해보면, 새로운 스크린은 오른쪽에서 왼쪽으로 이동됩니다.(화면이 보이지 않는 곳에서 시작하여 오프셋 만큼 도달)
이 전환이 일어나는 동안 각 프레임은 js 스레드는 main 스레드로 새로운 x 오프셋을 전달해야합니다. 만약 js 스레드가 바쁘다면, 전달할 수 없어서 프레임이 업데이트 되지 않고 애니메이션은 버벅 거리게 됩니다.
해결 방법중 하나는 js-based animation을 main 스레드에게 떠넘기는 것입니다. 이 것을 해결하는 것은 React Navigation 의 주 목표중 하나 입니다. React Navigation의 view는 네이티브 컴포넌트를 사용하고 네이티브 스레드에서 실행되는 60 FPS 애니메이션 구현을 위해 Animated 라이브러리를 사용합니다.
여기까지 RN 공식 DOCS의 Performance overview를 살펴보았습니다.
감사합니다.
출처: React native Docs
Get to know us better! Join our official channels below.
Telegram(EN) : t.me/Humanscape KakaoTalk(KR) : open.kakao.com/o/gqbUQEM Website : humanscape.io Medium : medium.com/humanscape-ico Facebook : www.facebook.com/humanscape Twitter : twitter.com/Humanscape_io Reddit : https://www.reddit.com/r/Humanscape_official Bitcointalk announcement : https://bit.ly/2rVsP4T Email : support@humanscape.io