@buivietphi/skill-mobile-mt 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +482 -0
- package/README.md +528 -0
- package/SKILL.md +1399 -0
- package/android/android-native.md +480 -0
- package/bin/install.mjs +976 -0
- package/flutter/flutter.md +304 -0
- package/humanizer/humanizer-mobile.md +295 -0
- package/ios/ios-native.md +182 -0
- package/package.json +56 -0
- package/react-native/react-native.md +743 -0
- package/shared/agent-rules-template.md +343 -0
- package/shared/ai-dlc-workflow.md +237 -0
- package/shared/anti-patterns.md +407 -0
- package/shared/architecture-intelligence.md +416 -0
- package/shared/bug-detection.md +71 -0
- package/shared/ci-cd.md +423 -0
- package/shared/claude-md-template.md +125 -0
- package/shared/code-review.md +133 -0
- package/shared/common-pitfalls.md +117 -0
- package/shared/document-analysis.md +167 -0
- package/shared/error-recovery.md +467 -0
- package/shared/observability.md +688 -0
- package/shared/offline-first.md +377 -0
- package/shared/performance-prediction.md +210 -0
- package/shared/platform-excellence.md +244 -0
- package/shared/prompt-engineering.md +705 -0
- package/shared/release-checklist.md +82 -0
- package/shared/testing-strategy.md +332 -0
- package/shared/ui-ux-mobile.md +667 -0
- package/shared/version-management.md +526 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Offline-First — Mobile Data Strategy
|
|
2
|
+
|
|
3
|
+
> On-demand. Load when: "offline", "offline-first", "cache", "sync", "local database", "persistence"
|
|
4
|
+
> Source: Mattermost, Immich, Expensify, Ignite
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌─────────────────────────────────────┐
|
|
12
|
+
│ UI Layer (reads local only) │
|
|
13
|
+
└──────────────┬──────────────────────┘
|
|
14
|
+
│
|
|
15
|
+
┌──────────────▼──────────────────────┐
|
|
16
|
+
│ Repository Layer (sync logic) │
|
|
17
|
+
└──────┬──────────────────────┬───────┘
|
|
18
|
+
│ │
|
|
19
|
+
┌──────▼──────┐ ┌─────────▼───────┐
|
|
20
|
+
│ Local DB │◄───►│ Remote API │
|
|
21
|
+
│ (primary) │ │ (sync only) │
|
|
22
|
+
└─────────────┘ └─────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Rule:** UI always reads local. API is sync-only, never primary.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## React Native — WatermelonDB (Reference Implementation)
|
|
30
|
+
|
|
31
|
+
### Schema
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// db/schema.ts
|
|
35
|
+
export const schema = appSchema({
|
|
36
|
+
version: 1,
|
|
37
|
+
tables: [
|
|
38
|
+
tableSchema({
|
|
39
|
+
name: 'posts',
|
|
40
|
+
columns: [
|
|
41
|
+
{ name: 'title', type: 'string' },
|
|
42
|
+
{ name: 'body', type: 'string' },
|
|
43
|
+
{ name: 'author_id', type: 'string' },
|
|
44
|
+
{ name: 'created_at', type: 'number' },
|
|
45
|
+
{ name: 'updated_at', type: 'number' },
|
|
46
|
+
{ name: 'is_synced', type: 'boolean' },
|
|
47
|
+
{ name: 'is_deleted', type: 'boolean' }, // soft delete required for sync
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Model
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export default class Post extends Model {
|
|
58
|
+
static table = 'posts';
|
|
59
|
+
static associations = {
|
|
60
|
+
comments: { type: 'has_many', foreignKey: 'post_id' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
@field('title') title!: string;
|
|
64
|
+
@field('body') body!: string;
|
|
65
|
+
@field('author_id') authorId!: string;
|
|
66
|
+
@readonly @date('created_at') createdAt!: Date;
|
|
67
|
+
@field('updated_at') updatedAt!: number;
|
|
68
|
+
@field('is_synced') isSynced!: boolean;
|
|
69
|
+
@field('is_deleted') isDeleted!: boolean;
|
|
70
|
+
|
|
71
|
+
async markAsDeleted() {
|
|
72
|
+
await this.update(post => {
|
|
73
|
+
post.isDeleted = true;
|
|
74
|
+
post.isSynced = false;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Sync Engine
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
export async function syncDatabase() {
|
|
84
|
+
await synchronize({
|
|
85
|
+
database,
|
|
86
|
+
pullChanges: async ({ lastPulledAt }) => {
|
|
87
|
+
const { changes, timestamp } = await api.get('/sync', {
|
|
88
|
+
params: { last_pulled_at: lastPulledAt },
|
|
89
|
+
});
|
|
90
|
+
return { changes, timestamp };
|
|
91
|
+
},
|
|
92
|
+
pushChanges: async ({ changes, lastPulledAt }) => {
|
|
93
|
+
await api.post('/sync', { changes, last_pulled_at: lastPulledAt });
|
|
94
|
+
},
|
|
95
|
+
migrationsEnabledAtVersion: 1,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function startBackgroundSync() {
|
|
100
|
+
const interval = setInterval(syncDatabase, 30000);
|
|
101
|
+
AppState.addEventListener('change', state => {
|
|
102
|
+
if (state === 'active') syncDatabase();
|
|
103
|
+
});
|
|
104
|
+
return () => clearInterval(interval);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Repository
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
export class PostRepository {
|
|
112
|
+
private posts = database.get<Post>('posts');
|
|
113
|
+
|
|
114
|
+
getAll() {
|
|
115
|
+
return this.posts.query(
|
|
116
|
+
Q.where('is_deleted', false),
|
|
117
|
+
Q.sortBy('created_at', Q.desc),
|
|
118
|
+
).fetch();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
observeAll() {
|
|
122
|
+
return this.posts.query(
|
|
123
|
+
Q.where('is_deleted', false),
|
|
124
|
+
Q.sortBy('created_at', Q.desc),
|
|
125
|
+
).observe();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async create(data: { title: string; body: string; authorId: string }) {
|
|
129
|
+
return database.write(async () => {
|
|
130
|
+
return this.posts.create(post => {
|
|
131
|
+
Object.assign(post, data);
|
|
132
|
+
post.isSynced = false;
|
|
133
|
+
post.isDeleted = false;
|
|
134
|
+
post.updatedAt = Date.now();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async update(post: Post, data: Partial<{ title: string; body: string }>) {
|
|
140
|
+
return database.write(async () =>
|
|
141
|
+
post.update(p => { Object.assign(p, data); p.isSynced = false; p.updatedAt = Date.now(); })
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async delete(post: Post) {
|
|
146
|
+
return database.write(async () => post.markAsDeleted());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Flutter — Drift + Riverpod
|
|
154
|
+
|
|
155
|
+
### Key differences from RN
|
|
156
|
+
|
|
157
|
+
```dart
|
|
158
|
+
// Table definition
|
|
159
|
+
class Posts extends Table {
|
|
160
|
+
TextColumn get id => text()();
|
|
161
|
+
TextColumn get title => text()();
|
|
162
|
+
BoolColumn get isSynced => boolean().withDefault(const Constant(false))();
|
|
163
|
+
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
|
|
164
|
+
@override Set<Column> get primaryKey => {id};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Watch (reactive, like observeAll)
|
|
168
|
+
Stream<List<Post>> watchAllPosts() => (select(posts)
|
|
169
|
+
..where((p) => p.isDeleted.equals(false))
|
|
170
|
+
..orderBy([(p) => Ordering.desc(p.createdAt)]))
|
|
171
|
+
.watch();
|
|
172
|
+
|
|
173
|
+
// Soft delete
|
|
174
|
+
Future<void> softDeletePost(String id) => (update(posts)).write(
|
|
175
|
+
PostsCompanion(id: Value(id), isDeleted: const Value(true), isSynced: const Value(false)),
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```dart
|
|
180
|
+
// Sync: check connectivity first
|
|
181
|
+
Future<void> sync() async {
|
|
182
|
+
final result = await Connectivity().checkConnectivity();
|
|
183
|
+
if (result == ConnectivityResult.none) return;
|
|
184
|
+
await _pushChanges();
|
|
185
|
+
await _pullChanges();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Background sync on connectivity restore
|
|
189
|
+
Connectivity().onConnectivityChanged.listen((result) {
|
|
190
|
+
if (result != ConnectivityResult.none) sync();
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## iOS — SwiftData (iOS 17+) / Core Data
|
|
197
|
+
|
|
198
|
+
```swift
|
|
199
|
+
@Model final class Post {
|
|
200
|
+
@Attribute(.unique) var id: String
|
|
201
|
+
var title: String
|
|
202
|
+
var isSynced: Bool = false
|
|
203
|
+
var isDeleted: Bool = false
|
|
204
|
+
var updatedAt: Date?
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sync with NWPathMonitor
|
|
208
|
+
private func startNetworkMonitoring() {
|
|
209
|
+
monitor.pathUpdateHandler = { [weak self] path in
|
|
210
|
+
Task { @MainActor in
|
|
211
|
+
if path.status == .satisfied { await self?.sync() }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
monitor.start(queue: queue)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func sync() async {
|
|
218
|
+
guard isConnected else { return }
|
|
219
|
+
let unsynced = try await repository.fetchUnsynced()
|
|
220
|
+
for post in unsynced {
|
|
221
|
+
post.isDeleted
|
|
222
|
+
? try await apiClient.deletePost(post.id)
|
|
223
|
+
: try await apiClient.upsertPost(post)
|
|
224
|
+
post.isSynced = true
|
|
225
|
+
}
|
|
226
|
+
let remote = try await apiClient.fetchPosts()
|
|
227
|
+
for post in remote { try await repository.upsert(post) }
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Android — Room + WorkManager
|
|
234
|
+
|
|
235
|
+
```kotlin
|
|
236
|
+
@Entity(tableName = "posts")
|
|
237
|
+
data class PostEntity(
|
|
238
|
+
@PrimaryKey val id: String,
|
|
239
|
+
val title: String,
|
|
240
|
+
val isSynced: Boolean = false,
|
|
241
|
+
val isDeleted: Boolean = false,
|
|
242
|
+
val updatedAt: Long? = null,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
@Dao interface PostDao {
|
|
246
|
+
@Query("SELECT * FROM posts WHERE isDeleted = 0 ORDER BY createdAt DESC")
|
|
247
|
+
fun observeAll(): Flow<List<PostEntity>>
|
|
248
|
+
|
|
249
|
+
@Query("SELECT * FROM posts WHERE isSynced = 0")
|
|
250
|
+
suspend fun getUnsynced(): List<PostEntity>
|
|
251
|
+
|
|
252
|
+
@Query("UPDATE posts SET isDeleted = 1, isSynced = 0 WHERE id = :id")
|
|
253
|
+
suspend fun softDelete(id: String)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Use WorkManager for background sync (respects battery/network constraints)
|
|
257
|
+
fun triggerSync() {
|
|
258
|
+
val request = OneTimeWorkRequestBuilder<SyncWorker>()
|
|
259
|
+
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
|
260
|
+
.build()
|
|
261
|
+
WorkManager.getInstance(context).enqueue(request)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Conflict Resolution
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// 1. Last Write Wins (simple, most apps)
|
|
271
|
+
const resolve = (local: Post, remote: Post) =>
|
|
272
|
+
local.updatedAt > remote.updatedAt ? local : remote;
|
|
273
|
+
|
|
274
|
+
// 2. Field-Level Merge (collaborative editing)
|
|
275
|
+
const merge = (local: Post, remote: Post, base: Post) => ({
|
|
276
|
+
...base,
|
|
277
|
+
title: local.title !== base.title ? local.title : remote.title,
|
|
278
|
+
body: local.body !== base.body ? local.body : remote.body,
|
|
279
|
+
updatedAt: Date.now(),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// 3. Operational Transform (counters/accumulators)
|
|
283
|
+
const resolveCounter = (local: Counter, remote: Counter, base: Counter) => ({
|
|
284
|
+
...remote,
|
|
285
|
+
value: base.value + (local.value - base.value) + (remote.value - base.value),
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## UI Components
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Offline banner
|
|
295
|
+
export function OfflineBanner() {
|
|
296
|
+
const [isOffline, setIsOffline] = useState(false);
|
|
297
|
+
useEffect(() => NetInfo.addEventListener(s => setIsOffline(!s.isConnected)), []);
|
|
298
|
+
if (!isOffline) return null;
|
|
299
|
+
return <Banner message="Offline — changes sync when connected." />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Sync status
|
|
303
|
+
type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error';
|
|
304
|
+
const STATUS_CONFIG = {
|
|
305
|
+
synced: { icon: 'check-circle', color: 'green' },
|
|
306
|
+
pending: { icon: 'clock', color: 'orange' },
|
|
307
|
+
syncing: { icon: 'sync', color: 'blue' },
|
|
308
|
+
error: { icon: 'alert-circle', color: 'red' },
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Optimistic update with rollback
|
|
312
|
+
const updateOptimistic = async (id: string, updates: Partial<Post>) => {
|
|
313
|
+
const prev = queryClient.getQueryData(['posts', id]);
|
|
314
|
+
queryClient.setQueryData(['posts', id], old => ({ ...old, ...updates }));
|
|
315
|
+
try {
|
|
316
|
+
await repository.update(id, updates);
|
|
317
|
+
await syncDatabase();
|
|
318
|
+
} catch {
|
|
319
|
+
queryClient.setQueryData(['posts', id], prev);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Database Selection
|
|
327
|
+
|
|
328
|
+
| DB | Platform | Use when |
|
|
329
|
+
|----|----------|----------|
|
|
330
|
+
| **WatermelonDB** | React Native | Complex queries, observables, built-in sync |
|
|
331
|
+
| **MMKV** | React Native | Key-value only, speed critical |
|
|
332
|
+
| **Realm** | RN / Flutter / iOS | Cross-platform, reactive |
|
|
333
|
+
| **SQLite** | All | Full SQL control |
|
|
334
|
+
| **Drift** | Flutter | Type-safe, migrations, code gen |
|
|
335
|
+
| **SwiftData** | iOS 17+ | Simple models, native |
|
|
336
|
+
| **Core Data** | iOS | Complex relationships, migrations |
|
|
337
|
+
| **Room** | Android | Flow/LiveData integration |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Checklist
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
Data:
|
|
345
|
+
□ All data written to local DB first
|
|
346
|
+
□ Soft deletes (never hard delete)
|
|
347
|
+
□ isSynced flag per record
|
|
348
|
+
□ Conflict resolution strategy defined
|
|
349
|
+
□ Retry for failed syncs
|
|
350
|
+
|
|
351
|
+
UI:
|
|
352
|
+
□ Offline banner
|
|
353
|
+
□ Sync status indicator
|
|
354
|
+
□ Optimistic updates
|
|
355
|
+
□ Pull-to-refresh triggers sync
|
|
356
|
+
|
|
357
|
+
Sync:
|
|
358
|
+
□ Sync on app foreground
|
|
359
|
+
□ Sync on connectivity restore
|
|
360
|
+
□ Background sync (30s interval)
|
|
361
|
+
□ Sync doesn't block UI thread
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Anti-Patterns
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
❌ UI reads directly from API
|
|
370
|
+
❌ Blocking UI during sync
|
|
371
|
+
❌ Hard deletes (breaks sync)
|
|
372
|
+
❌ Syncing on every keystroke
|
|
373
|
+
❌ No retry for failed syncs
|
|
374
|
+
❌ No offline indicator
|
|
375
|
+
❌ Assuming network is available
|
|
376
|
+
❌ Losing data on conflict
|
|
377
|
+
```
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Performance Prediction — Simulate Before Deploy
|
|
2
|
+
|
|
3
|
+
> Calculate frame rates, bridge calls, and memory before deploying code.
|
|
4
|
+
|
|
5
|
+
## Performance Prophet Pattern
|
|
6
|
+
|
|
7
|
+
**Predict behavior BEFORE deployment** using mathematical models.
|
|
8
|
+
|
|
9
|
+
### Frame Budget Calculation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
TARGET: 60 FPS = 16.67ms per frame
|
|
13
|
+
PROMOTION: 120 FPS = 8.33ms per frame
|
|
14
|
+
|
|
15
|
+
BUDGET BREAKDOWN (React Native):
|
|
16
|
+
- JavaScript execution: 8ms
|
|
17
|
+
- Bridge calls: 3ms
|
|
18
|
+
- Native rendering: 4ms
|
|
19
|
+
- Layout: 1.67ms
|
|
20
|
+
|
|
21
|
+
RULE: Total < 16.67ms for 60 FPS
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Predict List Performance
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Given code:
|
|
28
|
+
<FlatList
|
|
29
|
+
data={items} // 50 items
|
|
30
|
+
renderItem={({ item }) => (
|
|
31
|
+
<Item
|
|
32
|
+
title={item.title} // 1 bridge call
|
|
33
|
+
image={item.image} // 1 bridge call
|
|
34
|
+
onPress={() => logEvent(item.id)} // 1 bridge call
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
// Calculate:
|
|
40
|
+
Bridge calls per item: 3
|
|
41
|
+
Total items: 50
|
|
42
|
+
Total bridge calls: 50 × 3 = 150 calls
|
|
43
|
+
Time per call: ~0.3ms
|
|
44
|
+
Total time: 150 × 0.3ms = 45ms per frame
|
|
45
|
+
|
|
46
|
+
// Prediction:
|
|
47
|
+
16.67ms (60 FPS budget) vs 45ms (actual)
|
|
48
|
+
Result: 16.67 / 45 = 0.37 → 37% of 60 FPS = 22 FPS
|
|
49
|
+
Verdict: ❌ JANK - Users will notice lag
|
|
50
|
+
|
|
51
|
+
// Auto-fix suggestions:
|
|
52
|
+
1. Use getItemLayout (saves layout calculations)
|
|
53
|
+
2. Memoize renderItem component
|
|
54
|
+
3. Defer logEvent to InteractionManager
|
|
55
|
+
4. Use native driver for animations
|
|
56
|
+
5. Implement virtualization (windowSize: 5)
|
|
57
|
+
|
|
58
|
+
Expected after fix: ~12ms per frame → 60 FPS ✓
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Predict Memory Usage
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
IMAGE CALCULATION:
|
|
65
|
+
- Image: 1000×1000 pixels
|
|
66
|
+
- Color depth: 4 bytes (RGBA)
|
|
67
|
+
- Memory: 1000 × 1000 × 4 = 4MB per image
|
|
68
|
+
|
|
69
|
+
List with 50 images:
|
|
70
|
+
- No optimization: 50 × 4MB = 200MB
|
|
71
|
+
- With thumbnail (200×200): 50 × 0.16MB = 8MB
|
|
72
|
+
- Verdict: Use thumbnails + lazy load
|
|
73
|
+
|
|
74
|
+
MEMORY BUDGET:
|
|
75
|
+
- iOS: ~120MB baseline
|
|
76
|
+
- Android: ~80MB baseline (varies by device)
|
|
77
|
+
- Target: < 150MB total for stability
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Predict Bundle Impact
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
NEW PACKAGE: moment.js
|
|
84
|
+
Size: 230KB minified
|
|
85
|
+
Bundle before: 2.1MB
|
|
86
|
+
Bundle after: 2.33MB
|
|
87
|
+
Impact: +11% bundle size
|
|
88
|
+
|
|
89
|
+
Alternatives:
|
|
90
|
+
- date-fns/esm: 50KB (modular)
|
|
91
|
+
- day.js: 2KB (minimal)
|
|
92
|
+
Recommendation: Use day.js → 98% size reduction
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick Calculations
|
|
96
|
+
|
|
97
|
+
### 1. FlatList Frame Rate
|
|
98
|
+
```
|
|
99
|
+
Formula: FPS = 16.67ms / (bridge_calls × 0.3ms + render_time)
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
- 100 items
|
|
103
|
+
- 5 bridge calls per item
|
|
104
|
+
- 2ms render time per item
|
|
105
|
+
|
|
106
|
+
Time per frame: (100 × 5 × 0.3ms) + (100 × 2ms) = 150ms + 200ms = 350ms
|
|
107
|
+
FPS: 16.67 / 350 = 0.048 → 4.8 FPS
|
|
108
|
+
|
|
109
|
+
Verdict: ❌ UNUSABLE - Must optimize
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. Animation Smoothness
|
|
113
|
+
```
|
|
114
|
+
Rule: Use native driver when possible
|
|
115
|
+
|
|
116
|
+
With native driver:
|
|
117
|
+
- Runs at 60 FPS (or 120 FPS ProMotion)
|
|
118
|
+
- No bridge calls per frame
|
|
119
|
+
- Smooth animations
|
|
120
|
+
|
|
121
|
+
Without native driver:
|
|
122
|
+
- JavaScript thread: ~16.67ms budget
|
|
123
|
+
- Each frame: ~2-3 bridge calls
|
|
124
|
+
- Often drops to 30-45 FPS
|
|
125
|
+
|
|
126
|
+
Verdict: Always use { useNativeDriver: true }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Network Overhead
|
|
130
|
+
```
|
|
131
|
+
API Response: 500KB JSON
|
|
132
|
+
Parse time: ~50ms (varies by device)
|
|
133
|
+
UI block: 50ms = 3 frames dropped
|
|
134
|
+
|
|
135
|
+
Optimization:
|
|
136
|
+
- Paginate: 50KB per page → 5ms parse
|
|
137
|
+
- Background thread: 0 frames dropped
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Platform-Specific Predictions
|
|
141
|
+
|
|
142
|
+
### React Native
|
|
143
|
+
```
|
|
144
|
+
KNOWN BOTTLENECKS:
|
|
145
|
+
1. Bridge calls: 0.3ms each
|
|
146
|
+
2. console.log: 10-50ms (dev mode)
|
|
147
|
+
3. Inline styles: Re-creates every render
|
|
148
|
+
4. Anonymous functions in render: Creates new reference
|
|
149
|
+
|
|
150
|
+
OPTIMIZATION PRIORITY:
|
|
151
|
+
1. Remove console.log (50ms → 0ms)
|
|
152
|
+
2. StyleSheet.create (5ms → 0.1ms)
|
|
153
|
+
3. useCallback/useMemo (prevents re-renders)
|
|
154
|
+
4. Native driver animations (60 FPS guaranteed)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Flutter
|
|
158
|
+
```
|
|
159
|
+
KNOWN BOTTLENECKS:
|
|
160
|
+
1. Build method: Should be < 8ms
|
|
161
|
+
2. setState: Triggers full rebuild
|
|
162
|
+
3. Large widget trees: O(n) complexity
|
|
163
|
+
|
|
164
|
+
OPTIMIZATION PRIORITY:
|
|
165
|
+
1. const constructors (immutable widgets)
|
|
166
|
+
2. ListView.builder vs ListView (O(visible) vs O(n))
|
|
167
|
+
3. RepaintBoundary (isolate repaints)
|
|
168
|
+
4. Selective rebuilds (Consumer vs rebuild all)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Prediction Workflow
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
BEFORE IMPLEMENTING:
|
|
175
|
+
1. Estimate bridge calls (RN) or build time (Flutter)
|
|
176
|
+
2. Calculate frame budget impact
|
|
177
|
+
3. Predict FPS: 16.67ms / total_time
|
|
178
|
+
4. If < 60 FPS: redesign or optimize first
|
|
179
|
+
|
|
180
|
+
AFTER IMPLEMENTING:
|
|
181
|
+
1. Profile with React DevTools / Flutter DevTools
|
|
182
|
+
2. Compare predicted vs actual
|
|
183
|
+
3. Adjust model if off by > 20%
|
|
184
|
+
4. Document learnings for next time
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Auto-Suggestions
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
If prediction shows < 60 FPS:
|
|
191
|
+
|
|
192
|
+
FOR LISTS:
|
|
193
|
+
✓ Use getItemLayout
|
|
194
|
+
✓ Memoize renderItem
|
|
195
|
+
✓ Reduce bridge calls
|
|
196
|
+
✓ Virtualization (windowSize)
|
|
197
|
+
✓ Thumbnail images
|
|
198
|
+
|
|
199
|
+
FOR ANIMATIONS:
|
|
200
|
+
✓ Use native driver
|
|
201
|
+
✓ Avoid layout animations
|
|
202
|
+
✓ Prefer transform/opacity
|
|
203
|
+
✓ Use InteractionManager
|
|
204
|
+
|
|
205
|
+
FOR MEMORY:
|
|
206
|
+
✓ Image optimization
|
|
207
|
+
✓ Lazy loading
|
|
208
|
+
✓ Pagination
|
|
209
|
+
✓ Clear caches
|
|
210
|
+
```
|