Hướng dẫn tối ưu hóa hiệu suất ứng dụng Flutter cho người mới bắt đầu
Đăng ngày 10/03/2024

Hiệu suất ứng dụng là một trong những yếu tố quan trọng nhất quyết định trải nghiệm người dùng. Đối với các ứng dụng Flutter, một hiệu suất tốt sẽ mang lại hoạt động mượt mà, thời gian phản hồi nhanh và tiêu thụ pin thấp hơn. Bài viết này sẽ cung cấp cho các lập trình viên mới bắt đầu với Flutter những hướng dẫn cụ thể để tối ưu hoá hiệu suất ứng dụng, từ những khái niệm cơ bản đến các kỹ thuật chuyên sâu.
Hiểu về hiệu suất trong Flutter

Trước khi đi vào các kỹ thuật tối ưu, hãy hiểu về cách Flutter hoạt động và những yếu tố ảnh hưởng đến hiệu suất:
Hai thread chính trong Flutter
- UI Thread (Main Thread): Xử lý đầu vào người dùng, xây dựng widget tree, xử lý các hoạt động Flutter Framework.
- Rasterizer Thread: Chuyển đổi layer tree thành pixel trên màn hình.
Hiểu về khung hình (Frame)
Flutter nhắm đến việc duy trì tốc độ 60 khung hình/giây (FPS), nghĩa là mỗi khung hình có khoảng 16.67ms để hoàn thành. Nếu quá thời gian này, ứng dụng sẽ bị giật (jank).
Flutter DevTools
Flutter DevTools là một bộ công cụ mạnh mẽ giúp bạn hiểu hiệu suất ứng dụng:
- Performance View: Hiển thị thời gian xây dựng và vẽ từng khung hình
- Flutter Inspector: Giúp kiểm tra widget tree và tối ưu hóa cấu trúc UI
- Memory View: Theo dõi việc sử dụng bộ nhớ của ứng dụng
- CPU Profiler: Phân tích việc sử dụng CPU
Chiến lược tối ưu hóa hiệu suất cho người mới bắt đầu
1. Tối ưu hóa widget rebuild
Sử dụng `const` constructor
Khi một widget được khai báo với từ khóa `const`, Flutter sẽ tái sử dụng instance đó thay vì tạo instance mới mỗi khi rebuild:
// Không tối ưu
Container(
color: Colors.red,
child: Text('Hello'),
)
// Đã tối ưu
const Container(
color: Colors.red,
child: Text('Hello'),
)
Tránh rebuild không cần thiết với `StatefulWidget`
Sử dụng đúng `setState()` để chỉ rebuild các phần UI thực sự cần thiết:
// Không tối ưu - rebuild cả widget
setState(() {
_data = newData;
_isLoading = false;
});
// Đã tối ưu - tách thành các setState riêng biệt
setState(() {
_data = newData;
});
setState(() {
_isLoading = false;
});
Tối ưu với `AnimatedBuilder`
Khi làm việc với animation, hãy giới hạn những gì được rebuild trong `AnimatedBuilder`:
// Không tối ưu
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _controller.value * 100,
height: _controller.value * 100,
child: MyComplexWidget(), // Được tạo lại mỗi frame
);
},
)
// Đã tối ưu
AnimatedBuilder(
animation: _controller,
child: MyComplexWidget(), // Được tạo một lần duy nhất
builder: (context, child) {
return Container(
width: _controller.value * 100,
height: _controller.value * 100,
child: child,
);
},
)
2. Tối ưu hóa danh sách (list)
Sử dụng `ListView.builder` thay vì `ListView`
`ListView` tạo tất cả các children cùng một lúc, trong khi `ListView.builder` chỉ tạo các item khi chúng được hiển thị trên màn hình:
// Không tối ưu cho danh sách lớn
ListView(
children: items.map((item) => ItemWidget(item)).toList(),
)
// Đã tối ưu cho danh sách lớn
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
Sử dụng `const` cho các item cố định
Nếu các item trong danh sách không thay đổi, hãy sử dụng `const` constructor:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return const ListTile(
leading: Icon(Icons.check),
title: Text('Constant item'),
);
},
)
Sử dụng kỹ thuật phân trang (pagination)
Đối với danh sách rất lớn, hãy tải và hiển thị dữ liệu theo từng trang:
class _PaginatedListState extends State<PaginatedList> {
List<Item> items = [];
int page = 1;
bool isLoading = false;
bool hasMore = true;
@override
void initState() {
super.initState();
_loadMore();
}
Future<void> _loadMore() async {
if (isLoading || !hasMore) return;
setState(() => isLoading = true);
// Giả sử API trả về 20 items mỗi trang
final newItems = await api.getItems(page: page);
setState(() {
isLoading = false;
page++;
items.addAll(newItems);
hasMore = newItems.length == 20;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length + (hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= items.length) {
_loadMore();
return const Center(child: CircularProgressIndicator());
}
return ItemWidget(items[index]);
},
);
}
}
3. Tối ưu hóa hình ảnh
Sử dụng đúng định dạng và kích thước hình ảnh
- Sử dụng định dạng phù hợp: WebP, PNG cho hình ảnh cần độ trong suốt, JPEG cho ảnh phức tạp
- Nén hình ảnh trước khi đưa vào ứng dụng
- Sử dụng kích thước hình ảnh phù hợp với kích thước hiển thị
Sử dụng `cached_network_image` cho hình ảnh từ mạng
// Cài đặt package:
// flutter pub add cached_network_image
// Sử dụng:
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
Lazy loading hình ảnh
Chỉ tải hình ảnh khi cần thiết, đặc biệt trong danh sách:
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// Hình ảnh chỉ được tải khi item hiển thị trên màn hình
return ListTile(
leading: Image.network(items[index].imageUrl),
title: Text(items[index].title),
);
},
)
4. Tối ưu hóa đồng bộ và bất đồng bộ
Sử dụng `async`/`await` đúng cách
// Không tối ưu - chặn UI thread
void loadData() {
final data = api.fetchDataSync(); // Blocking call
setState(() {
_data = data;
});
}
// Đã tối ưu - không chặn UI thread
Future<void> loadData() async {
final data = await api.fetchData(); // Non-blocking call
setState(() {
_data = data;
});
}
Sử dụng `compute` cho các tác vụ nặng
Khi cần xử lý dữ liệu lớn, hãy di chuyển công việc ra khỏi UI thread:
// Hàm phải ở top-level hoặc static
List<Result> _processDataInBackground(List<Data> data) {
// Xử lý dữ liệu phức tạp...
return results;
}
// Trong widget
Future<void> processData() async {
final results = await compute(_processDataInBackground, _data);
setState(() {
_results = results;
});
}
Sử dụng `FutureBuilder` và `StreamBuilder`
Xử lý các trạng thái bất đồng bộ một cách thanh lịch:
FutureBuilder<UserData>(
future: _fetchUserData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return Text('No data available');
}
final userData = snapshot.data!;
return UserProfileWidget(userData);
},
)
5. Giảm kích thước ứng dụng
Tối ưu hóa assets
- Chỉ bao gồm assets cần thiết
- Nén hình ảnh
- Sử dụng vector graphics (SVG) khi có thể
- Sử dụng font icons thay vì nhiều hình ảnh riêng lẻ
# pubspec.yaml
flutter:
assets:
- assets/images/
fonts:
- family: MyCustomIcons
fonts:
- asset: assets/fonts/MyCustomIcons.ttf
Sử dụng `--split-debug-info`
Giảm kích thước của debug symbols:
flutter build apk --split-debug-info=build/debug-info
Sử dụng ProGuard (Android)
Thêm cấu hình ProGuard vào `android/app/build.gradle`:
android {
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
Công cụ đo lường hiệu suất
Sử dụng Flutter DevTools Performance View
Để mở Flutter DevTools:
- Chạy ứng dụng trong chế độ debug
- Chạy lệnh: `flutter pub global run devtools`
- Mở trình duyệt tại địa chỉ URL hiển thị
- Kết nối với ứng dụng đang chạy
Phân tích hiệu suất với `flutter run --profile`
flutter run --profile
Chế độ profile cung cấp hiệu suất gần với bản release nhưng vẫn cho phép đo lường.
Sử dụng Flutter Performance Overlay
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = false;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showPerformanceOverlay: true,
home: HomePage(),
);
}
}
Các kỹ thuật tối ưu nâng cao
1. Sử dụng `RepaintBoundary`
Ngăn chặn việc vẽ lại không cần thiết bằng cách cô lập phần UI thay đổi thường xuyên:
Stack(
children: [
// Phần không thay đổi thường xuyên
BackgroundWidget(),
// Phần thay đổi thường xuyên, được cô lập
RepaintBoundary(
child: AnimatedWidget(),
),
],
)
2. Giảm độ phức tạp của widget tree
// Không tối ưu - widget tree quá sâu
Container(
padding: EdgeInsets.all(8),
child: Container(
color: Colors.white,
child: Container(
padding: EdgeInsets.all(4),
child: Text('Hello'),
),
),
)
// Đã tối ưu - gộp các thuộc tính
Container(
padding: EdgeInsets.all(12),
color: Colors.white,
child: Text('Hello'),
)
3. Sử dụng `SliverList` cho danh sách phức tạp
CustomScrollView(
slivers: [
SliverAppBar(
title: Text('My App'),
floating: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 100,
),
),
],
)
4. Sử dụng hỗ trợ GPU cho animation
// Sử dụng transform để animation được xử lý bởi GPU
Transform.translate(
offset: Offset(animation.value * 100, 0),
child: MyWidget(),
)
5. Sử dụng `InheritedWidget` và `Provider` đúng cách
Chia nhỏ model để tránh rebuild toàn bộ UI:
// Không tối ưu - một model duy nhất
class AppModel extends ChangeNotifier {
User user;
List<Product> products;
ShoppingCart cart;
// Khi có thay đổi, toàn bộ UI phụ thuộc vào model sẽ rebuild
void updateUser(User newUser) {
user = newUser;
notifyListeners();
}
}
// Đã tối ưu - chia nhỏ model
class UserModel extends ChangeNotifier {
User user;
void update(User newUser) {
user = newUser;
notifyListeners();
}
}
class ProductsModel extends ChangeNotifier {
List<Product> products;
void update(List<Product> newProducts) {
products = newProducts;
notifyListeners();
}
}
Các lỗi hiệu suất phổ biến và cách khắc phục
1. CPU Jank (Giật)
Dấu hiệu: UI không phản hồi, hoạt ảnh không mượt
Nguyên nhân có thể:
- Quá nhiều công việc trên UI thread
- Phức tạp Widget tree quá cao
- Rebuild không cần thiết
Cách khắc phục:
- Sử dụng `compute` cho các tác vụ nặng
- Đơn giản hóa Widget tree
- Sử dụng `const` constructor và tối ưu rebuild
2. GPU Jank
Dấu hiệu: Hoạt ảnh bị giật nhưng CPU không quá tải
Nguyên nhân có thể:
- Quá nhiều layers phải vẽ
- Hiệu ứng đồ họa phức tạp (shadow, blur)
Cách khắc phục:
- Giảm số lượng layer với `RepaintBoundary`
- Đơn giản hóa hiệu ứng đồ họa
- Sử dụng các thuộc tính được GPU hỗ trợ (transform, opacity)
3. Rò rỉ bộ nhớ
Dấu hiệu: Ứng dụng sử dụng ngày càng nhiều bộ nhớ theo thời gian
Nguyên nhân có thể:
- Không hủy đăng ký listener/subscription
- Lưu trữ dữ liệu lớn không cần thiết
Cách khắc phục:
- Hủy đăng ký tất cả listeners trong `dispose()`
- Sử dụng weak references cho dữ liệu lớn
- Giải phóng tài nguyên khi không cần thiết
class _MyWidgetState extends State<MyWidget> {
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_subscription = stream.listen(_handleData);
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
Quy trình tối ưu hiệu suất theo bước
Bước 1: Đo lường hiệu suất hiện tại
- Sử dụng Flutter DevTools để xác định vấn đề
- Tập trung vào các khu vực có vấn đề hiệu suất
Bước 2: Tối ưu hóa Widget tree
- Sử dụng `const` constructors
- Giảm độ sâu Widget tree
- Tách biệt các phần UI bằng `StatefulWidget`
Bước 3: Tối ưu hóa danh sách và hình ảnh
- Sử dụng `ListView.builder` thay vì `ListView`
- Thêm phân trang cho danh sách lớn
- Tối ưu hóa kích thước và định dạng hình ảnh
Bước 4: Tối ưu hóa animation
- Sử dụng `RepaintBoundary` cho các animation
- Sử dụng các thuộc tính GPU hỗ trợ
- Tránh animation phức tạp trên các thiết bị cũ
Bước 5: Tối ưu hóa logic nghiệp vụ
- Di chuyển các tác vụ nặng sang các Isolate
- Tách biệt UI và logic nghiệp vụ
- Sử dụng caching để giảm tính toán
Kết luận
Tối ưu hóa hiệu suất là một quá trình liên tục, không phải là một hoạt động một lần duy nhất. Sử dụng các kỹ thuật và công cụ được nêu trong bài viết này, ngay cả những lập trình viên Flutter mới cũng có thể tạo ra các ứng dụng có hiệu suất cao.
Hãy nhớ rằng mục tiêu là tạo trải nghiệm người dùng mượt mà, không phải để có số liệu tốt trong các công cụ đo lường. Luôn kiểm tra ứng dụng trên nhiều thiết bị thực tế, đặc biệt là các thiết bị cũ hoặc có hiệu suất thấp để đảm bảo trải nghiệm tốt cho tất cả người dùng.
Bạn đã gặp phải vấn đề hiệu suất nào trong ứng dụng Flutter của mình? Bạn đã áp dụng kỹ thuật tối ưu hóa nào hiệu quả nhất? Hãy chia sẻ kinh nghiệm của bạn trong phần bình luận bên dưới!