앱 성능 측정 및 개선 방법

diense_kk·2023년 12월 31일
0

Developer

목록 보기
8/8
post-thumbnail

잘 만들어진 앱의 기준은 사용자마다 관점이 달라서 대답하기 어렵다. 하지만 버벅이는 앱이 완성도가 높다는 평가를 듣기란 쉬운 일이 아니다 그러니 우선 우리는 최소한 버벅이지 않는 앱을 만들어보자

Jank 오류 현상

Flutter는 Skia engine을 통해 Widget을 생성하고 제거한다. 일반적으로 Skia engine은 60Hz로 동작하는 Ticker와 함께 화면을 업데이트 하므로, 우리는 16.7ms 안에 Rendering을 완료해야 한다.
만약 우리가 Ticker의 주기에 맞추어 Rendering을 끝내지 못하게 된다면 새로운 UI가 그려지지 않고 화면은 업데이트 되지 않게 되며, 앱이 버벅인다고 느끼게 될 것이다. 이렇게 앱이 주사율을 맞추지 못하고 버벅이는 것을 사용자가 보는 걸 Jank라고 한다.

화면 업데이트 주기(점선)에 맞추어 Render가 완료되어야 하지만 그렇지 못하면 Jank이다.

Flutter로 Rendering Performance 측정

Rendering Performance를 개선하기 위해서는 앱에서의 Jank의 발생을 감지하고 Rendering을 모니터링 해야 한다. Flutter에서는 이를 위해 Performance Profiling도구를 지원해준다. Flutter를 profile 모드로 실행하고, Dart DevTools를 이용해서 화면 렌더링에 17ms를 초과하면 붉은 색 막대로 표현되며, 개선이 필요함을 알려준다.

Performance 탭의 4개의 그래프

UI
Dart VM을 통해 Dart코드를 실행하는 스레드이다. Widget Tree를 Layer Tree로 변환하고, 이를 Raster 스레드로 보내는 역할을 한다.

Raster
Layer Tree를 받아 GPU와 통신하여 UI를 업데이트 합니다. Skia engine이 이 스레드에서 실행됩니다. 개발자는 Raster 스레드나 스레드의 데이터에 접근 할 수 없습니다.

Platform
각 플랫폼 별 스레드입니다. Performance Overlay에는 표시되지 않습니다.

I/O
입출력을 담당하고, Performance Overlay에는 표시되지 않습니다.

Jank가 나타나는 대부분의 경우는 UI와 Raster 스레드 모두에서 오랜 시간이 걸리는 경우와, UI 스레드는 아무런 문제가 없는데 Raster 스레드에서 문제가 나타나는 경우가 있다.
전자의 경우는 UI 스레드의 변경이 너무 잦거나, Widget Tree가 너무 자주 변경되거나, UI 스레드에서 무거운 작업이 수행되고 있는 경우이다.
후자의 경우에는 saveLayer, Opacity, Shadow, Clip이 원인일 가능성이 높습니다. 정적이지 않은 이미지 캐싱도 많은 Cost를 소모이다.

Flutter documents 기반 성능 개선 방법

1. build 메소드를 최대한 가볍게, 최대한 덜 호출되도록

  • build 메소드는 UI 변경이 있을 때 언제든지 다시 호출 될 수 있는 함수이다. 그러므로 build에서 비용이 많이 드는 작업을 해서는 안된다. FutureBuilder를 사용 할 때, Future에 대한 caching을 하지 않았다면 매번 새롭게 future을 대기하게 된다.

  • 하나의 큰 위젯보다는 작게 나누어진 여러 위젯이 낫다. 하나의 큰 위젯의 구현부를 메소드로 나누는 건 아무런 도움이 되지 않습니다. StatelessWidget과 statefulWidget은 자체적인 caching 시스템을 이용하기 떄문에 변경 없는 rebuild의 비용이 크지 않다.

  • build 메소드를 가능한 한 적게 호출 되도록 하는 방법은 위젯을 const로 만드는 것입니다. const Widget은 상위 위젯이 rebuild 되어도, 변경이 없다면 다시 build 되지 않습니다.

2. build 메소드를 최대한 가볍게, 최대한 덜 호출되도록

Flutter에서 실제로 위젯이 그려지는 과정은 다음과 같다.
각각 Widget Tree/Element Tree/Render Tree

개발자가 구성한 Widget Tree는 Element Tree로 변환된다.
Element Tree는 논리적 구조인 Widget Tree와 실제로 Rendering되는 구조인 Render Tree를 Mapping하는 Element의 Tree이다. Widget은 createElement()를 통해 Element를 생성합니다. 이 때 생성되는 Element가 바로 BuildContext 이다. Element는 createRenderObject() 를 통해 RenderObject를 생성하고, Layer를 생성하게 된다. 이렇게 생성된 LayerTree가 Raster 스레드로 전달되어 위젯이 그려지게 된다.

따라서 rebuild 과정에서 Widget Tree에 변경이 없다면, Render Tree에서의 최소한의 변경 사항만 생기게 된다. 하지만 Widget Tree에 변경이 생긴다면 하위 Tree 전체를 다시 작성하게 되어 UI 스레드에 부하가 걸리게 된다.

3. 가능하다면 lazy load

대부분의 경우에서 ListView보다는 ListView.builder가 낫다.ListView.builder는 화면에 표시되는 위젯만 동적으로 build하고, 화면에서 사라지면 메모리에 유지하지 않는다. 하지만 ListView는 맨 처음 build될 때 모든 위젯을 빌드해 Jank를 유발한다.

4. 무거운 작업은 Isolate

Dart는 기본적으로 단일 스레드 언어이다. 다트는 오직 하나의 Isolate만을 가지고 시작하고 Async와 Await은 병렬 작업이 아니다.

Isolate는 Memory와 하나의 스레드, EventLoop를 가진 독립적인 실행 공간이다. eventLoop는 microTaskQueue와 eventQueue로 이루어져 있으며, 기본적으로 microTaskQueue가 우선권을 갖게된다.

void eventLoop() {
  while(microTaskQueue.isNotEmpty) {
    fetchFirstMicroTaskFromQueue();
    executeThisMicroTask();
  } if (eventQueue.isNotEmpty) {
    fetchFirstEventFromQueue();
    executeThisEventRelatedCode();
  }
} // microTaskQueue에 있는 모든 task를 실행한 후에 eventQueue의 task를 실행한다.

모든 I/O, Gesture, Tap, Timer, Future, 다른 Isolates로부터의 message 등의 모든 Event는 eventQueue에 add된 후에, eventLoop에 의해 순차적으로 처리된다.

EventQueue에 등록된 event가 eventLoop에 의해 순차적으로 실행되며, 각 event의 handler/task가 스레드에서 처리된다.

async와 await 역시 UI Thread에서 계산이 되기 때문에 무거운 작업을 UI Thread에서 처리하게 된다면 Jank가 발생할 수 있다.

그럼 어떻게 UI Thread에 부하를 주지 않고 무거운 작업을 처리할 수 있을까요? 병렬적으로 작업을 처리하기 위해서는 Isolate를 생성해야한다. 생성된 Isolate는 별개의 메모리와 EventLoop에 따라 동작하기 때문에 UI Processing에 영향을 주지 않으면서 동작할 수 있다. 한편 Isolate는 이름 그대로 다른 Isolate로부터 완전히 ‘격리’되어 있다. 따라서 새로 만든 Isolate는 메인 Isolate와 port를 통해 메시지를 주고 받는 방식으로 동작하게 된다.

Isolate는 별개의 메모리를 할당 받고, 메인 Isolate와 메시지를 주고 받는 오버헤드가 있기 때문에 무조건 Isolate를 생성해서 처리하는 건 좋은 방식이 아니다. 보통 UI가 업데이트 되는 주기인 16ms를 기준으로 잡고, 이보다 오래 걸리는 작업은 Isolate를 통해 처리하는 게 좋다. 16ms보다 길어질 수 있는 sync 작업의 대표적인 예시는 Json 직렬화이다.

5. 꼭 필요할 때만 effect를 사용

effect들은 Raster 스레드와 GPU에 부하를 주게 된다.

0개의 댓글