Coaspe

Flutter - Performance best practices 본문

Flutter/번역

Flutter - Performance best practices

Coaspe 2023. 2. 17. 14:02

Minimize expensive operations

몇몇 연산자들은 다른 연산자들보다 더 많은 리소스를 소모합니다. 앱의 UI를 어떻게 디자인하고 구현하는지가 얼마나 효율적으로 앱이 작동하는지 큰 영향을 줍니다.

Control build() cost

UI를 디자인 할 때 항상 생각해야할 것이 있습니다.

  • 상위 위젯을 rebuild 할 때 해당 위젯의 build()가 자주 호출될 수 있으므로, build() 메소드에서 반복적이고 비용이 많이 드는 작업을 피하세요.
  • build() 함수가 있는 지나치게 큰 단일 위젯을 만드는 것을 피하세요. 캡슐화뿐만 아니라 위젯들이 어떻게 변하는지에 따라 위젯을 다른 위젯들로 분할하세요.
    • State 객체에서 setState()가 호출되면, 모든 하위 위젯들이 rebuild 됩니다. 그러므로, 실제로 UI를 변경해야 하는 하위 트리 부분에 setState() 호출을 localize(지역화)하세요. 트리의 작은 부분에 대한 변경 내용이 포함된 경우 트리의 높은 곳에서 setState()를 호출하지 않도록 만드세요.
    • 이전 프레임과 동일한 하위 위젯 인스턴스가 다시 발생할 경우 모든 하위 위젯을 다시 빌드하기 위한 순회 작업(traversal)은 중지됩니다. 이런 테크닉은 애니메이션이 자식 서브트리에 영향을 미치지 않는 애니메이션을 최적화하기 위해 flutter 내에서 많이 사용됩니다. 애니메이팅 할 때 후손들이 rebuilding하는 것을 피하기 위해 앞의 원칙을 지키는TransitionBuilder 패턴과 source code for SlideTransition 을 참고하세요.
    • 가능하다면 const 생성자를 사용하세요. const 생성자는 Flutter가 rebuild의 대부분의 작업에서 short-circuit 할 수 있게 해줍니다. const 사용하는 것을 잊지 않기 위해, flutter_lints 패키지를 사용하세요. 더 자세한 정보는, flutter_lints migration guide를 참고하세요.
    • 재사용 가능한 UI의 피스를 만들고 싶다면, 함수말고 StatelessWidget를 사용하세요.

더 자세한 정보를 얻고 싶다면, 다음을 참고하세요:


Use saveLayer() thoughtfully

몇몇 Flutter 코드는 UI의 다양한 비쥬얼 효과를 구현하기 위해 비싼 연산자인 saveLayer()를 사용합니다. 심지어 코드가 명시적으로 saveLayer()를 호출하지 않더라도, 같이 사용하는 다른 위젯들이나 패키지들이 해당 함수를 호출할 수도 있습니다. 아마 당신의 앱은 필요 이상으로 saveLayer()를 호출할 것입니다; 불필요한 saveLayer()의 호출은 jank를 발생시킵니다.

Why is saveLayer expensive?

saveLayer()를 호출하는 것은 오프스크린 버퍼를 할당하고, 오프스크린 버퍼로 컨텐츠를 가져오는 것은 렌더 타겟의 변화(render target switch)를 트리거 할 수도 있습니다. GPU는 빨리빨리 작동하기를 원하지만, 렌더 타겟의 변화는 GPU가 일시적으로 해당 스트림으로 리디렉션하고, 다시 원래 스트림으로 리디렉션하게 합니다. (대충 렌더의 타겟이 왔다 갔다 한다는 말). 모바일 GPU에서 이것은 렌더링 처리량에 특히 지장을 줍니다.

When is saveLayer required?

런타임에서, 만약 서버로부터 오는 다양한 형태를 동적으로 디스플레이 해야하는 경우, 각 도형이 투명하게 겹칠 수도 있으므로(아닐 수도 있음) saveLayer()를 사용해야 합니다.

Debugging calls to saveLayer

앱에서 saveLayer()를 직접 또는 간접적으로 호출하는 빈도를 어떻게 알 수 있을까요? saveLayer() 메소드는 DevTools timeline에 이벤트를 발생시킵니다; 어떤 장면에서 saveLayer를 사용하는 시기를 알고 싶다면 DevTools Performance view에서 PerformanceOverlayLayer.checkerboardOffscreenLayers 스위치를 확인하세요.

Minimizing calls to saveLayer

saveLayer 호출을 피할 수 있을까요? 그러기 위해선 비쥬얼 효과를 어떻게 생성할 건지 다시 생각해 볼 필요가 있습니다.

  • 만약 당신의 코드에서 saveLayer를 호출한다면, 그것을 줄이거나 없앨 수 있을까요? 예를 들면, 두개의 도형이 투명도 없이 겹쳐있는 UI가 있다고 해봅시다:
    • 만약 둘이 항상 같은 방향, 투명도로 같은 크기만큼 겹쳐있다면, 당신은 겹쳐진 반투명 도형이 어떻게 생겼는지 미리 계산할 수 있습니다. 그것을 캐시하고, saveLayer()를 호출하는 대신 사용하세요. 이것은 미리 계산 할 수 있는 모든 정적인 도형에 사용 가능합니다.
    • 도형이 겹치는 것을 피하기 위해 painting 로직을 리팩토링 할 수 있나요?
  • 만약 saveLayer의 호출이 당신이 만들지 않은 패키지에서 발생한다면, 패키지의 오너와 연락해서 해당 호출이 꼭 필요한지 물어보세요. 해당 호출들을 줄이거나, 제거 할 수 있나요? 만약 그렇지 못한다면, 다른 패키지를 찾거나 알아서 작성하세요.

saveLayer()을 호출 할 수도 있는 위젯들은 다음과 같습니다:

  • ShaderMask
  • ColorFilter
  • Chip—disabledColorAlpha != 0xff라면 saveLayer()를 호출합니다.
  • Text—overflowShader가 있다면, saveLayer()를 호출합니다.

Minimize use of opacity and clipping

Opacity는 clipping 처럼 비싼 연산자입니다. 다음은 Opacity를 사용할때 유용한 팁입니다:

  • Opacity는 정말 필요할 때만 사용하세요. Opacity위젯을 사용하는 것 보다 빠르게 Opacity를 이미지에 직접 적용하는 예인 Transparent image를 사용 할 수도 있습니다.
  • Opacity 위젯으로 간단한 도형이나 텍스트를 감싸는 것 보다, semitransparent color로 해당 위젯들을 만드는 보통 더 빠릅니다.(하지만 이것은 모양에 겹치는 비트가 없는 경우에만 작동합니다.)
  • 페이드하는 이미지를 구현하고 싶다면, GPU의 fragment shader를 사용하여 점진적인 opacity를 적용하는 FadeInImage 위젯을 사용해보세요. 더 자세히 알고 싶다면, Opacity 문서를 참고하세요.
  • ClippingsaveLayer()를 호출하지 않습니다(Clip.antiAliasWithSaveLayer를 명시적으로 같이 요청하지 않는 한). 그렇기 때문에 이런 연산자들은 Opacity만큼 비싸지 않습니다. 하지만 clipping은 여전히 비용이 있으므로 주의해서 사용하세요. 디폴트로, clipping은 사용안함으로 설정되어 있습니다.(Clip.none) 그렇기 때문에 필요하다면 명시적으로 enable로 설정해야 합니다.
  • 둥근 모서리를 가지는 사각형을 만들고 싶다면, clipping rectangle을 사용하기보다, borderRadius 속성을 사용하세요.

Implement grids and lists thoughtfully

당신이 어떻게 grid와 list를 구현하는지에 따라 앱의 성능 문제를 일으킬 수도 있습니다. 이번 섹션에서는 grid와 list를 만들 때 가장 좋은 방법을 소개하고, 앱에서 과도한 레이아웃 패스를 사용하는지 여부를 확인하는 방법을 다룹니다.

Be lazy!

큰 grid와 list를 만들 때, 콜백을 가지는 lazy builder 메서드를 사용하세요. 그렇게 하면 처음 빌드 할 때 화면에 보이는 부분 만큼한 빌드되게 할 수 있습니다.

 

더 많은 자료와 예는 다음을 참고하세요:

Avoid intrinsics

intrinsic 패스가 grid, list에 어떻게 문제를 발생시키는지는 다음 섹션을 확인하세요.


Minimize layout passes caused by intrinsic operations

Flutter 코딩을 많이 해봤으면, UI를 생성 할 때 how layout and constraints 작동하는지 잘 알것입니다. Flutter의 기본 레이아웃 원칙을 기억할 것입니다: Constraints go down. Sizes go up. Parent sets position.(제한은 부모에서 자식으로. 사이즈는 자식에서 부모로, 부모가 위치를 정해준다.)

 

Gride나 list 같은 위젯들은 layout 프로세스가 비쌀 때가 있습니다. Flutter는 위젯에 대해 하나의 레이아웃 패스를 수행하려고 노력하지만 때때로, 두번째 패스(intrinsic pass)가 필요하고 그것은 속도를 느리게 합니다.

What is an intrinsic pass?

예를 들자면 Intrinsic 패스는 모든 셀들을 가장 큰 크기나, 작은 크기로 설정할 때 발생합니다.(또는 모든 셀들을 폴링해야하는 비슷한 연산들을 수행 할 때)

 

예를 들면, Card들로 이루어진 큰 grid를 생각해봅시다. grid는 동일한 사이즈를 가지는 셀들을 가져야 합니다. 그래서 그리드의 루트(위젯 트리에서)에서 시작해서 레이아웃 코드는 grid에 있는 card(visible card 뿐만 아니라 다른 card들 에게도)에게 intrinsic 사이즈를 리턴하게 하며 패스를 수행합니다. 그 사이즈는 위젯이 선호하고, 제한이 없다고 가정합니다. 이 정보로 framework는 동일한 셀 크기를 결정하고, 모든 grid의 셀을 다시 방문하며 어떤 사이즈를 사용할건지 물어봅니다.

Debugging intrinsic passes

과도한 intrinsic 패스를 사용하는지 확인하고 싶다면, DevTools의 Track layouts option 을 enable로 설정하세요. 그리고 앱의 stack trace에서 얼마나 많은 layout 패스가 수행되는지 확인하세요. 트래킹이 가능해지면, intrinsic 타임라인 이벤트가 ‘$runtimeType intrinsics’로 라벨링 될 것 입니다.

Avoiding intrinsic passes

Intrinsic 패스를 피하는 몇가지 옵션이 있습니다.

  • 고정된 크기로 셀을 설정하세요.
  • 특정 셀을 "anchor"로 선택하세요. 모든 셀의 크기는 이 셀에 비례합니다. 자식 앵커를 먼저 배치한 다음 그 주위에 다른 자식 앵커들을 배치하는 사용자 지정 렌더 객체를 작성하세요.

어떻게 layout이 작동하는지 더 깊게 알고 싶다면, Flutter architectural overviewlayout and rendering 섹션을 확인하세요.


Build and display frames in 16ms

Building과 rendering에 사용되는 2개의 쓰레드가 있기 때문에, building을 위해 16ms, 60Hz 디스플레이에 rendering을 위한 16ms가 사용이 가능합니다. 지연 시간이 우려되는 경우(더 부드럽게 표현하고 싶다면) 16ms 이하로 프레임을 빌드하고 표시합니다. 즉 8ms 이하에서 build되고 8ms 이하로 렌더링되며, 총 16ms 이하에서 렌더링됩니다.

 

만약 profile mode 모드에서 프레임이 총 16ms 이하로 잘 렌더링 된다면, 일부 성능 저하가 발생하더라도 성능에 대해 걱정할 필요는 없지만 프레임을 최대한 빨리 구축하고 렌더링하는 것을 목표로 해야 합니다. 왜 그럴까요?

  • 프레임 렌더 타임을 16ms 밑으로 낮추는 것은 외적 차이를 만들지 않지만, 배터리 수명을 향상시키고 발열 문제를 해결합니다.
  • 타겟으로하는 가장 성능이 안 좋은 디바이스도 고려해야 합니다.
  • 요즘에는 120fps를 지원하는 디바이스가 많기 때문에, 더 부드러운 사용을 제공하기 위해 총 8ms 이하로 프레임을 렌더링해야 할 때가 있습니다.

왜 60fps가 부드러운 visual experience를 만드는지 궁금하다면, Why 60fps?를 보세요.


Pitfalls

앱의 성능을 조절해야 한다거나, UI가 기대만큼 부드럽지 않다면, DevTools Performance view가 도움이 될 것입니다!

 

또한, 사용하는 IDE를 위한 Flutter 플러그인도 도움이 될 것입니다. Flutter Performance window에서, Show widget rebuild information 체크 박스를 enable 해보세요. 이 피쳐는 프레임이 16ms 이상으로 렌더되거나 디스플레이 되는 것을 감지해줍니다. 가능하다면, 플러그인은 그것과 관련된 팁의 링크를 제공해 줄 것입니다.

 

다음 행동들은 앱의 성능에 좋지않은 영향을 끼칩니다.

  • Opacity 위젯 사용을 피하세요. 특히 애니메이션에서 사용하는 것을 피하세요. 대신 AnimatedOpacity or FadeInImage을 사용하세요. 더 자세하게 알고 싶다면, Performance considerations for opacity animation을 참고하세요.
  • AnimatedBuilder을 사용할 때, 애니메이션과 의존성이 없는 서브트리를 builder 함수로 넘기지 마세요. 이 서브트리는 애니메이션의 틱 마다 rebuild 됩니다. 대신, 그 서브 트리를 한 번만 빌드하고, AnimatedBuilder의 child로 넘기세요. 더 자세하게 알고 싶다면, Performance optimizations를 참고하세요.
  • 애니메이션 안에서 clipping하는 것을 피하세요. 가능하다면, 애니메이션 전에 미리 clip 된 이미지를 사용하세요.
  • 만약 대부분의 자식들이 화면에 바로 보여지지 않는다면 build cost를 줄이기 위해, 생성자에 구체적인 자식들의 List (Column() or ListView() 같은)를 사용하는 것을 피하세요.

Resources

For more performance info, check out the following resources:

 


정리

  1. 큰 build() 함수를 가지는 위젯을 피하자
  2. 가능하다면 const 생성자를 사용하자.
  3. 재사용할 UI를 만들고 싶다면, 헬퍼 함수보단 위젯을 사용하자
  4. saveLayer의 불필요한 호출을 줄이자
  5. Opacity 위젯의 사용을 되도록이면 줄이자.
  6. clipping 위젯도 borderRadius 속성을 사용하자.
  7. Grid나 list를 만들 때 보이지 않을 자식들을 미리 빌드시키지 말자
  8. Intrinsic 패스를 최대한 줄이자

'Flutter > 번역' 카테고리의 다른 글

Flutter - Platform-specific behaviors and adaptations  (0) 2023.02.17
Flutter - Animations #Introduction  (0) 2023.02.17
Flutter - Animations #Overview  (0) 2023.02.17
Flutter - Bloc # Architecutre  (0) 2023.02.17
Flutter - bloc #Core Concepts  (0) 2023.02.17
Comments