协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
mobile-performance【开发与工程】Use this skill when the user asks about mobile performance optimization, app slow, jank, frame drops, memory leaks, startup time, battery drain, bundle size, or profiling tools.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Diagnose and optimize mobile app performance across rendering, memory, startup time, battery drain, and bundle size using platform profiling tools.
User request includes: mobile performance, app slow, jank, frame drop, memory leak mobile, app startup, battery drain, app size, profiling mobile.
A markdown document containing:
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
4096 tokens
What is the user complaint?
├── "App is slow to start" → Startup Performance
│ ├── Cold start >2s → Baseline Profiles (Android), reduce dynamic frameworks (iOS)
│ ├── Warm start >800ms → Cache last state, avoid heavy deserialization on resume
│ └── Hot start >400ms → Defer non-critical SDK init, lazy-load modules
├── "App is janky/stuttering" → Rendering Performance
│ ├── List scrolling drops frames → Virtualize list (ListView.builder, FlatList, LazyColumn)
│ ├── Animation stutters → Use GPU-composited properties (transform, opacity), avoid layout
│ └── Navigation transition jank → Pre-warm next screen, lazy-load screen content
├── "App crashes/uses too much memory" → Memory Performance
│ ├── Heap grows without shrinking → Find retain cycle or subscription leak
│ ├── OOM on image-heavy screens → Downsample images, use disk cache, purge on low memory
│ └── Memory grows with navigation → Check screen deallocation, weak ref patterns
├── "App drains battery" → Battery Performance
│ ├── Background network activity → Batch network requests, use push instead of polling
│ ├── Wake locks held too long → Minimize background work, use WorkManager (Android)
│ └── Location tracking always on → Use significant-change or region monitoring
└── "App download is too large" → Bundle Size
├── IPA >80MB / APK >30MB → Code splitting, remove unused resources, asset optimization
└── Download >100MB cellular warning → On-demand resources, app thinning, deferred downloads
┌──────────────────────────────────────────┐
│ User Interface Layer │
│ Rendering: 60fps (16ms/frame budget) │
│ 120fps for ProMotion (8ms/frame budget) │
├──────────────────────────────────────────┤
│ Application Logic Layer │
│ Main thread work: <100ms per chunk │
│ Heavy work: offload to background thread│
├──────────────────────────────────────────┤
│ Data Layer │
│ Network: cache-first strategy │
│ Storage: indexed queries, lazy load │
├──────────────────────────────────────────┤
│ Platform Bridge │
│ Flutter: 4-8ms Dart-to-native overhead │
│ RN: 10-50ms JS-to-native bridge latency │
│ Native: 0ms (direct API calls) │
└──────────────────────────────────────────┘
Use platform profiling tool to measure frame times, memory allocations, startup duration, and battery impact.
Address jank with virtualization (ListView.builder, FlatList, LazyColumn), const widgets, RepaintBoundary, and proper cell reuse.
Eliminate retain cycles, cancel network requests on dispose, use weak references, and profile with heap snapshots.
Implement Baseline Profiles (Android), reduce dynamic framework loading (iOS), and defer non-critical initialization.
Enable code splitting, remove unused dependencies, use --split-debug-info, and analyze bundle composition.
Minimize background work, batch network requests, use platform background task APIs (BGTaskScheduler on iOS, WorkManager on Android), and reduce wake lock duration. Monitor energy impact in Xcode Instruments Energy Log and Android Battery Historian.
Integrate performance monitoring SDK (Firebase Performance, Datadog RUM, Sentry Performance) to collect real-world metrics. Set up dashboards for key metrics: cold start time, frame drop rate, peak memory, network latency. Configure alerts for metric degradation beyond 20% of baseline.
performance_budgets:
startup:
cold_start:
target: "<2s to interactive on mid-range device"
metrics: ["Time to first frame", "Time to interactive", "Fully loaded time"]
tools: ["Firebase Performance", "Macrobenchmark (Android)", "MetricKit (iOS)"]
warm_start:
target: "<800ms to interactive"
optimization: "Avoid heavy de-serialization on resume, cache last state"
runtime:
frame_rate:
target: "60fps consistent (120fps for ProMotion devices)"
measurement: "Frame timing instrumentation — not just perceived smoothness"
memory:
target: "<200MB peak heap on mid-range"
measurement: "Heap snapshots at key user flows — leak detection per screen"
bundle:
app_size_android:
target: "<30MB APK (or AAB equivalent)"
reduction: "R8 optimization, --split-debug-info, remove unused resources"
app_size_ios:
target: "<80MB IPA"
reduction: "Asset catalog optimization, remove unused architectures, Bitcode"
download:
target: "<100MB initial download from store (to avoid cellular warning)"
profiling_workflow:
step_1_establish_baseline:
activity: "Run performance tests on reference device (mid-range, not flagship)"
tools: ["Xcode Instruments", "Android Profiler", "Flutter DevTools", "Flipper"]
metrics: ["Startup time", "Frame rate", "Peak memory", "Bundle size"]
step_2_identify_bottlenecks:
activity: "Profile each key user flow — launch, list scroll, image load, navigation"
signs_of_trouble: ["Frame drops >3%", "Heap growing without GC", "Startup >3s"]
step_3_hypothesize_and_fix:
activity: "Form hypothesis about root cause, implement fix, re-profile"
common_fixes:
startup: "Defer SDK init, lazy-load modules, Baseline Profiles (Android)"
jank: "Virtualized lists, const widgets, efficient re-render triggers"
memory: "Weak refs, dispose subscriptions, image cache limits"
step_4_verify:
activity: "Profile again with same conditions — confirm fix without regression"
threshold: "Improvement >10% in target metric"
step_5_monitor:
activity: "Add performance monitoring to production release"
tools: ["Firebase Performance", "Datadog RUM", "New Relic Mobile", "Sentry Performance"]
// LazyColumn with stable keys
LazyColumn {
items(orders, key = { it.id }) { order ->
key(order.id) { OrderCard(order) }
}
}
// Avoid recomposition — use derivedStateOf
val visibleCount = remember(orders) {
derivedStateOf { orders.count { it.isVisible } }
}
// Compose modifier optimization
@Composable
fun OrderCard(order: Order) {
Column(
modifier = Modifier
.drawWithContent { /* custom draw */ } // Only redraws when needed
.then(Modifier.padding(8.dp))
) { ... }
}
// Use immutable state holders
@Immutable
data class OrderUiState(val orders: List<Order>, val isLoading: Boolean)
// Cell reuse — UIKit
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell")!
return cell
}
// SwiftUI — LazyVStack instead of VStack
ScrollView {
LazyVStack {
ForEach(orders, id: \.id) { order in
OrderRow(order: order)
}
}
}
// Reduce view body recomputation
struct OrderListView: View {
let orders: [Order]
var body: some View {
List(orders, id: \.id) { order in
NavigationLink(value: order) {
OrderRow(order: order)
}
}
}
}
// EquatableView for diffing
EquatableView(content: OrderRow(order: order))
.equatable()
// Avoid rebuilds — use const widgets
const OrderCard(order: order); // Only rebuilds when order ref changes
// RepaintBoundary for complex widgets
RepaintBoundary(child: MapWidget());
// Use ListView.builder — not ListView(children: [])
ListView.builder(itemCount: items.length, itemBuilder: ...);
// Use ValueListenableBuilder for targeted rebuilds
ValueListenableBuilder<int>(
valueListenable: counterNotifier,
builder: (context, count, child) {
return Text('Count: $count');
},
);
// Avoid passing function references that cause rebuilds
// Bad: builder: (context) => MyWidget(onTap: () => handleTap())
// Good: builder: (context) => MyWidget(onTap: handleTap)
// FlatList with proper config
<FlatList
data={orders}
renderItem={renderItem}
getItemLayout={getItemLayout}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
initialNumToRender={10}
/>
// UseMemo for expensive computations
const total = useMemo(() => computeTotal(orders), [orders]);
// UseCallback for stable function references
const handlePress = useCallback((id: string) => {
navigation.navigate('OrderDetail', { id });
}, [navigation]);
// React.memo for preventing unnecessary re-renders
const OrderRow = React.memo(({ order }: { order: Order }) => {
return <Text>{order.title}</Text>;
});
// InteractionManager for deferring non-critical work
InteractionManager.runAfterInteractions(() => {
loadNonCriticalData();
});
// Android — Coil with size constraints
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.size(400, 300) // Downsample to display size
.crossfade(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null
)
// Coil memory cache config
val imageLoader = ImageLoader.Builder(context)
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(0.25) // 25% of available heap
.build()
}
.build()
// iOS — Kingfisher or Nuke for disk-backed cache
KFImage.url(URL(string: url))
.resizable()
.setProcessor(DownsamplingImageProcessor(size: CGSize(width: 400, height: 300)))
.cacheMemoryOnly(false) // Enable disk cache
.memoryCacheOptions(.init(memory costLimit: 50 * 1024 * 1024)) // 50MB limit
// URLSession cache config
let config = URLSessionConfiguration.default
config.urlCache = URLCache(memoryCapacity: 50_000_000, diskCapacity: 200_000_000)
// Flutter — cached_network_image
CachedNetworkImage(
imageUrl: url,
width: 200,
height: 150,
memCacheWidth: 400, // Cache downsampled version
memCacheHeight: 300,
placeholder: (_, __) => const Shimmer(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
)
// React Native — fast-image with cache
import FastImage from 'react-native-fast-image';
<FastImage
style={{ width: 200, height: 150 }}
source={{ uri: url, priority: FastImage.priority.normal }}
resizeMode={FastImage.resizeMode.contain}
/>
# Convert PNG/JPG to WebP for 25-35% size reduction
cwebp -q 80 input.png -o output.webp
# Android supports WebP natively (API 18+)
# iOS supports WebP via SDWebImage/Kingfisher
# AVIF offers 50% better compression than JPEG — Android 12+, iOS 16+
// Baseline Profiles (src/main/baseline-prof.txt)
// Improves cold start by 30-40%
HSPLcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/example/app/features/orders/OrderListScreen;-><init>()V
HSPLcom/example/app/data/repository/OrderRepository;->getOrders()Lkotlinx/coroutines/flow/Flow;
// Startup tracing
@ExperimentalTraceFolksApi
class MainApplication : Application() {
override fun onCreate() {
trace("Application.onCreate") {
super.onCreate()
// Init critical SDKs first
trace("init.crashlytics") { FirebaseCrashlytics.init(this) }
// Defer non-critical
trace("init.analytics") { /* defer */ }
}
}
}
// App Startup library for deterministic init ordering
// InitializationProvider in AndroidManifest with dependencies
// Reduce dynamic framework linking — prefer static frameworks
// In Build Settings: Mach-O Type = Static Library
// Defer non-critical initialization
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
initNonCriticalSDKs()
}
}
}
}
}
// Use +load/+initialize sparingly — they block startup
// Avoid +load in Objective-C categories
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
// Defer SDK init to after first frame
SchedulerBinding.instance.addPostFrameCallback((_) {
_initNonCriticalSDKs();
});
}
// Use deferred loading for heavy packages
import 'package:heavy_package' deferred as heavy;
Future<void> loadHeavyPackage() async {
await heavy.loadLibrary();
}
// Use inline requires for heavy imports
const HeavyModule = {
get instance() {
return require('./HeavyModule').default;
}
};
// InteractionManager for deferring
InteractionManager.runAfterInteractions(() => {
Analytics.init();
});
// Hermes engine for faster startup (enabled by default in RN 0.70+)
// hermes.enable = true in metro.config.js
// Android: Baseline Profiles
// In src/main/baseline-prof.txt
Lcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V
Lcom/example/app/features/orders/OrderListScreen;-><init>()V
// iOS: Reduce dynamic framework loading
// Move non-essential frameworks to optional
// Flutter: Defer non-critical init
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
// Defer SDK init to after first frame
SchedulerBinding.instance.addPostFrameCallback((_) {
_initNonCriticalSDKs();
});
}
# Flutter: Use --split-debug-info
flutter build apk --split-debug-info=build/debug-info
# RN: Remove unused packages
npx react-native-bundle-analyzer
# iOS: App Thinning
# Enable in Xcode: Assets.car per-device slicing, on-demand resources
// OkHttp — connection pooling is default, configure for your use case
val client = OkHttpClient.Builder()
.connectionPool(ConnectionPool(maxIdleConnections = 5, keepAliveDuration = 5, TimeUnit.MINUTES))
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build()
// Axios — conditional requests
api.interceptors.request.use(async (config) => {
const cached = await cache.get(config.url);
if (cached?.etag) {
config.headers['If-None-Match'] = cached.etag;
}
return config;
});
api.interceptors.response.use(async (response) => {
if (response.status === 304) {
return cache.get(response.config.url); // Return cached response
}
if (response.headers.etag) {
await cache.set(response.config.url, { ...response, etag: response.headers.etag });
}
return response;
});
class BatchedApiClient {
final _queue = <String>{};
Timer? _timer;
void fetch(String id) {
_queue.add(id);
_timer ??= Timer(Duration(milliseconds: 50), _flush);
}
Future<void> _flush() async {
final ids = _queue.toList();
_queue.clear();
_timer = null;
// Single batch request instead of N individual requests
final results = await api.getOrdersBatch(ids);
for (final result in results) {
cache.put(result.id, result);
}
}
}
// OkHttp — gzip is automatic for response bodies
// Request body compression:
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
val compressedBody = request.body?.let { body ->
compressGzip(body)
}
if (compressedBody != null) {
chain.proceed(request.newBuilder()
.header("Content-Encoding", "gzip")
.method(request.method, compressedBody)
.build())
} else {
chain.proceed(request)
}
}
.build()
// Prefer significant-change over continuous
// Bad
manager.startUpdatingLocation()
// Good
manager.startMonitoringSignificantLocationChanges()
// Or region monitoring
manager.startMonitoring(for: region)
// Reduce update frequency
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters // Not Best
manager.distanceFilter = 100 // Only update every 100m
// Android — Fused Location Provider with balanced power
val request = LocationRequest.Builder(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 60000)
.setMinUpdateDistanceMeters(100f)
.build()
// Android — WorkManager (not raw AlarmManager or Service)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
// iOS — BGTaskScheduler (not background fetch)
let request = BGProcessingTaskRequest(identifier: "com.example.sync")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false // false = allow on battery
try? BGTaskScheduler.shared.submit(request)
Polling: N requests per hour, each wakes radio for ~10-20 seconds
→ 30 requests/hour × 15s = 450s of radio time per hour
→ Battery drain: HIGH
Push notification: 0 background network, push wakes app only when needed
→ Battery drain: NEGLIGIBLE
JobScheduler (Android) / BGTaskScheduler (iOS) for opportunistic batchingInteractionManager and useMemoWidget build() creating new objects each call triggers unnecessary recomposition. Extract constantskey(): Items in LazyColumn without keys cause incorrect animations and full recompositiondidReceiveMemoryWarning (iOS) or onTrimMemory (Android) get killeddisposeBag / AutoDisposeBitmap.recycle() or libraries like Coil/Glideflutter clean, npx react-native-analyzer--split-debug-info (Flutter), strip (iOS), minifyEnabled (Android)dependency:analysis or Gradle dependency treex86_64 emulator libs in release. Filter with abiFiltersacquire(timeout) with timeout, always release in finallyonDisappear / Disappear| Platform | Rendering | Memory Model | Startup | Bundle Size |
|---|---|---|---|---|
| Native (Swift/Kotlin) | Direct GPU access, 60fps guaranteed | ARC/GC, fine-grained control | Fastest (native code) | Smallest |
| Flutter | Skia/Impeller engine, 60-120fps | Dart GC, widget tree overhead | Moderate (engine init ~200ms) | Moderate (engine ~5MB) |
| React Native | JS-to-native bridge, 60fps typical | JS GC, bridge serialization overhead | Slowest (JS engine init + bundle parse) | Large (JS bundle + RN lib) |
| Ionic/Capacitor | WebView rendering, 60fps achievable | WebView heap, limited control | Slow (WebView init ~200-600ms) | Large (WebView + Ionic libs) |
| Kotlin Multiplatform | Native performance per platform | Platform-native memory model | Native fast startup | Platform-native size |
| Tool | Platform | Use Case |
|---|---|---|
| Xcode Instruments | iOS | Frame timing, allocations, energy, network |
| Android Studio Profiler | Android | CPU, memory, network, energy |
| Flutter DevTools | Flutter | Widget rebuild, frame analysis, memory |
| React DevTools + Flipper | React Native | Component tree, network, layout |
| Systrace (Android) | Android | System-level trace, thread scheduling |
| MetricKit (iOS) | iOS | Production performance data aggregation |
| Firebase Performance | Cross-platform | Production monitoring with traces |
| Sentry Performance | Cross-platform | Transaction tracing with error context |
| Datadog RUM | Cross-platform | Real user monitoring with session replay |
| New Relic Mobile | Cross-platform | APM with distributed tracing |
| PerfDog | Cross-platform | Frame rate, temperature, battery on device |
| GTmetrix / Lighthouse | PWA | Web performance audit for PWA mode |
Hand off to stack-specific skill for implementation fixes.