
- Published on
- ·7 min read
App很卡?你可能用了錯誤的方式開發
- Authors

- Name
- Bert / DOTUNE
- Developer
還在寫後端的時候,有一次我在自己的安卓機上編譯了公司的 App,同事湊過來看了一眼,驚嘆道:「好流暢啊。」
那時候 iOS 機型普遍還停在 60Hz 更新率,看來即使是負責這個 App 的同事,也沒見過它真正順暢跑起來的樣子(笑)。
但常看到有人在知乎上說 Flutter 很卡,動不動拿閒魚出來舉例,說它有渲染上的卡頓。我自己用閒魚的時候,是沒觀察到什麼渲染問題——所以我覺得這部分可能存在一些誤會。
話雖如此,Flutter 確實有幾個地方,一個沒注意就會踩出卡頓。下面是我在實戰中覺得最常見、也最該優先處理的幾項。
一、三個基礎誤區(速覽)
1. setState() 在頂層,整頁重建
// ❌ Scaffold 層 setState → 下面全部重建
// ✅ 拆出會變的部分,不變的加 const
Flutter 的渲染是宣告式的:當你呼叫 setState(),Framework 會從那個節點開始,一路往下重建整棵子樹。把 setState() 放在 Scaffold 層,等於告訴 Flutter:「整頁都dirty了,全部重畫。」實際上你只是要更新 tab 的高亮狀態。
解法很簡單——把會變的部分拆成獨立的 StatefulWidget,不變的部分標上 const。Flutter 看到 const 的 widget 會直接跳過不重建。
2. 不用 const
// ❌ Text('Hello') ← 每次 build 都 new
// ✅ const Text('Hello')
Flutter 的 widget tree 分三層:Widget(設定)→ Element(管理生命週期)→ RenderObject(實際繪製)。加 const 告訴 Flutter 這個 widget 的設定永不改變,Framework 可以在 element 層直接復用上一個 frame 的結果,完全跳過重建。
const 沒有成本、沒有副作用、lint 就能自動補,呃而且Flutter官方早都在語法層面上約束了這一點就是了。
3. 在 debug 模式測性能
// ❌ flutter run (debug 模式,JIT 編譯,比 release 慢 2-10 倍)
// ✅ flutter run --profile(AOT 編譯,接近 release 的真實性能)
Debug 模式是 Flutter 的預設。它用 JIT 編譯、掛著 debug assert 和 hot reload 基礎設施,跑起來比 release 慢 2~10 倍。商店會擋 debug 版,所以不怕 CI/CD 誤傳,但很多人連本地測試都懶得切 flutter run --profile,結果花了一整天在優化根本不存在的卡頓。
一句話記住:const 鎖死、setState 下推、測性能用 --profile。這三個先做好,大部分「莫名卡頓」就消失了。
二、你寫的不是 Widget,是函數(重點)
// ❌ 很多人這樣寫
Widget _buildHeader() => Padding(
padding: EdgeInsets.all(8),
child: Text('Header'),
);
// ✅ 應該這樣
class Header extends StatelessWidget {
const Header({super.key});
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8),
child: Text('Header'),
);
}
}
為什麼差那麼多?
- 函數不能被
const修飾 → 每次重建 - DevTools 的 Widget Tree 看不到函數 → 你無法追蹤它重建了幾次
- Hot reload 對函數無效 → 改了也不會更新
- Flutter 框架層的優化(element 復用)對函數完全失效
三、build() 裡藏了昂貴的操作(重點)
// ❌ 每次 rebuild 都執行
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; // 鍵盤彈出也觸發
final items = jsonDecode(hugeJsonString); // 阻塞 UI 線程
final dropdownItems = list.map((e) => // 1000 個 item
DropdownMenuItem(value: e, child: Text(e))
).toList();
return ...;
}
// ✅ 快取 / 預計算 / scope 縮小
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context); // 只在 size 變時觸發
final dropdownItems = _cachedItems; // initState 時建好
return ...;
}
build() 在 Flutter 裡被呼叫的頻率比你想像的高很多——鍵盤彈出、螢幕旋轉、動畫每一幀、甚至只是一個小小的 setState,都可能觸發 rebuild。如果你在 build() 裡面放了昂貴的操作,這些東西就會被反覆執行,UI 線程直接被吃掉。

最常見的幾種:JSON 解析、大量 list 的 map/where、MediaQuery.of(context).size(鍵盤一彈整個畫面就重算)。解法一句話:build() 裡面只放「從既有 state 組出 UI」的邏輯,其他都移出去。 解析放 initState、快取用 field、MediaQuery.sizeOf 取代 MediaQuery.of(context).size 來限縮重建範圍。
四、忘記 dispose,記憶體慢慢吃光(重點)
// ❌ 這些人永遠不會被釋放
class _MyPageState extends State<MyPage> {
final _scrollController = ScrollController();
final _animationController = AnimationController(...);
final _streamSubscription = someStream.listen((_) {});
// ❌ 沒有 dispose()!
}
// ✅
void dispose() {
_scrollController.dispose();
_animationController.dispose();
_streamSubscription.cancel();
super.dispose();
}
Controller、StreamSubscription、Animation——這些東西在創建之後會持續佔用資源,直到你主動釋放。Flutter 不會幫你處理,Widget 被移出 tree 也不代表資源被回收了。
如果你在 State 裡面 new 了一個 ScrollController 或 AnimationController,卻沒有在 dispose() 裡面對應清理,頁面退出之後這些東西還活著——記憶體慢慢累積,使用者滑來滑去 App 就越來越卡。
特別注意:BuildContext 存在 async 回調裡、GlobalKey 重複使用、無限 push 不 pop 的導航棧,都是記憶體洩漏的來源。
附:第三方套件的效能陷阱
pub.dev 上的套件,不等於可以放心直接用。坦白說,我遇過八成以上的 widget package 都得自己先處理過一輪,才敢放進正式專案裡。上專案之前,花時間在最小 demo 裡單獨驗過一遍,總是划算的。
上面這些問題其實都還算容易注意到並解決,但是更細節的優化,包括動作交互的處理,和其他平臺相關的API交互的數據往來,都還有很多細節需要注意,這裏就先不贅述了。