-
Flutter 성능은 16ms 프레임 예산을 쓰는 방식입니다AI Agent/flutter 2026. 7. 3. 09:00728x90반응형

스크롤이 걸리적거린다는 제보로 열어 본 타임라인 "스크롤할 때 손끝에 뭔가 미묘하게 걸리적거려요." 이런 제보가 제일 곤란합니다.
버벅인다는 것도 아니고, 재현 절차가 있는 것도 아니고, 그냥 손가락이 한 번씩 턱턱 걸린다는 겁니다.
제 기기에서는 아무리 쓸어내려도 매끈하게 넘어갔습니다.
그래서 며칠을 "기분 탓 아닐까" 하고 미뤄뒀습니다.밤에 다른 작업이 없어서, 큰맘 먹고 DevTools 타임라인을 켜고 리스트를 위아래로 반복해서 긁었습니다.
Performance 화면에 프레임이 막대 하나씩 쌓이는데, 초록 사이에 빨간 막대가 규칙적으로 섞여 올라왔습니다.
그때 처음으로 16ms라는 숫자를 눈으로 봤습니다.빨간 막대는 16ms를 넘겼다는 신호입니다
60fps로 그리려면 프레임 하나를 대략 16ms 안에 끝내야 합니다.
이 안에서 위젯을 다시 만들고(build), 크기를 재고(layout), 칠하고(paint), GPU로 넘겨 래스터라이즈까지 마쳐야 합니다.
이 예산을 넘기면 그 프레임은 제때 못 나오고, 화면은 한 박자 멈췄다가 따라옵니다.
사용자가 "걸린다"고 느끼는 정체가 이 한 박자입니다.막대 하나를 눌러 안을 펼치면 build, layout, paint 구간이 색깔별로 쌓여 있습니다.
저는 초록 프레임과 빨간 프레임을 번갈아 눌러 보면서, 빨간 쪽은 유독 build 구간이 길쭉하게 늘어나 있다는 걸 봤습니다.
위젯을 그리는 시간 자체가 그 프레임에서만 부풀어 있었던 겁니다.
어디서 부푸는지는 아직 몰랐지만, 적어도 "느리다"가 아니라 "이 프레임의 build가 무겁다"까지는 좁혀졌습니다.제가 그동안 성능을 rebuild 횟수로만 봤다는 걸 여기서 알았습니다.
위젯이 몇 번 다시 그려지느냐는 여러 지표 중 하나일 뿐이고, 정작 태우는 건 한 프레임 안에서 벌어지는 무거운 일 한 방일 때가 많습니다.
예산은 횟수가 아니라 시간입니다.const를 붙이며 삼십 분을 날렸습니다
처음엔 당연히 rebuild가 너무 잦은 줄 알았습니다.
리스트 아이템 위젯마다const가 빠진 곳을 찾아 붙이고, 쪼갤 수 있는 서브트리를 분리했습니다.
삼십 분쯤 그 작업을 하고 다시 타임라인을 켰는데, 빨간 막대는 그대로였습니다.
오히려 살짝 늘어난 것 같기도 했습니다(이건 제 착각이었을 수도 있습니다).const를 붙이면 rebuild가 줄어드는 건 맞습니다.
다만 그건 위젯 객체를 새로 안 만든다는 얘기지, 그 위젯의 build가 가볍다는 보장은 아닙니다.
저는 이 둘을 같은 것으로 착각했습니다.
rebuild 횟수를 줄이는 데만 매달려서, 정작 한 번의 build가 왜 이렇게 오래 걸리는지는 안 들여다봤던 겁니다.빨간 프레임 하나를 눌러서 안을 펼쳐 보고 나서야 원인이 보였습니다.
build 안에서 매 아이템마다 JSON 문자열을 통째로 파싱하고 있었습니다.
서버가 준 raw 응답을 위젯이 들고 있다가, 그릴 때마다jsonDecode를 돌렸던 겁니다.class HeavyTile extends StatelessWidget { const HeavyTile({super.key, required this.rawJson}); final String rawJson; @override Widget build(BuildContext context) { final data = jsonDecode(rawJson); // 스크롤 프레임마다 다시 파싱됩니다 return Text(data['title'] as String); } }리스트 하나에 아이템이 수십 개, 스크롤 중에는 그 build가 계속 재호출됩니다.
rebuild가 잦은 게 문제가 아니라, 그 잦은 build 하나하나가 무거웠던 겁니다.
const를 아무리 붙여도 이 파싱이 build 경로에 남아 있는 한 예산은 계속 터졌습니다.파싱을 build 밖으로 한 칸 옮겼습니다
고친 건 별게 없었습니다.
파싱을 위젯 build에서 빼내서, 데이터를 리스트에 넣기 전에 한 번만 하도록 옮겼습니다.class HeavyTile extends StatelessWidget { const HeavyTile({super.key, required this.title}); final String title; // 이미 파싱된 값만 받습니다 @override Widget build(BuildContext context) => Text(title); }같은 데이터인데, 파싱이 스크롤 밖으로 나가니 타임라인의 빨간 막대가 거의 사라졌습니다.
제보하신 분 기기에서만 두드러졌던 이유도 그제야 납득이 됐습니다.
제 기기는 CPU가 빨라서 그 파싱을 16ms 안에 욱여넣었고, 느린 기기는 못 넣어서 튀었던 겁니다.
같은 코드가 기기마다 다르게 보인 게 아니라, 예산의 여유가 달랐던 거죠.고치고 나서 느린 기기를 하나 빌려 다시 스크롤해봤습니다.
예전엔 그 기기에서 손끝에 확실히 걸리던 게, 이제는 초록 막대만 줄줄이 올라왔습니다.
저는 엄지로 화면을 위아래로 몇 번을 더 긁어봤습니다.
걸리던 자리를 손끝이 기억하고 있어서, 그 지점만 자꾸 눌러 밀었는데 매번 밋밋하게 미끄러졌습니다.
엄지 끝이 헛도는 느낌이 들 때까지 긁고 나서야 폰을 내려놨습니다.
무거운 위젯을 잘게 쪼갠 것도 아니고, 파싱 한 번을 build 바깥으로 옮긴 것뿐인데 말입니다.그런데 고친 리스트의 타임라인을 오래 켜두고 긁다 보면, 아주 가끔 빨간 막대가 하나씩 튀어 오릅니다.
파싱 때처럼 규칙적이지도 않고, 손끝엔 안 잡힐 만큼 드뭅니다.
그게 이미지 디코드인지 다른 무엇인지, 그 막대는 아직 안 열어봤습니다.
초록이 이만큼 늘어난 지금은, 저 하나가 눈에 밟히면서도 오늘은 여기까지, 하고 창을 닫게 됩니다.원문 기준
원문 기준: https://docs.flutter.dev/perf/best-practices
확인일: 2026-07-01.
프레임 관련 권장 사항은 버전마다 조금씩 바뀌니, 올리기 전에 문서를 한 번 더 봅니다.728x90반응형'AI Agent > flutter' 카테고리의 다른 글
Hot reload는 상태를 보존하지만 모든 변경을 반영하지 않습니다 (0) 2026.07.03 Flutter 터치는 콜백이 아니라 Gesture Arena에서 결정됩니다 (0) 2026.07.03 Flutter 에러는 try/catch 하나로 잡히지 않습니다 (0) 2026.07.02 Flutter 앱의 경계는 Dart 코드 바깥에도 있습니다 (0) 2026.07.02 Flutter에서 Container(width)가 안 먹는 이유 (0) 2026.07.02