npx skills add https://github.com/jezweb/claude-skills --skill react-native适用于 React Native + Expo 应用的高性能和架构模式。规则按影响程度排序——在接触 MEDIUM 之前先修复 CRITICAL 级别的问题。
这是一个起点。随着你构建更多移动应用,这项技能将不断增长。
列表是 React Native 中的头号性能问题。卡顿的滚动会毁掉整个应用体验。
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 使用 ScrollView 渲染数据 | <ScrollView> 会一次性渲染所有项目 | 使用 <FlatList> 或 <FlashList> —— 虚拟化,只渲染可见项目 |
| 缺少 keyExtractor | FlatList 没有 keyExtractor → 导致不必要的重新渲染 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
keyExtractor={(item) => item.id} —— 为每个项目提供稳定的唯一键 |
| 复杂的 renderItem | renderItem 中的昂贵组件在每次滚动时都会重新渲染 | 用 React.memo 包装,提取到单独的组件中 |
| renderItem 中的内联函数 | renderItem={({ item }) => <Row onPress={() => nav(item.id)} />} | 提取处理函数:const handlePress = useCallback(...) |
| 没有 getItemLayout | FlatList 在滚动时测量每个项目(开销大) | 为固定高度的项目提供 getItemLayout:(data, index) => ({ length: 80, offset: 80 * index, index }) |
| FlashList | FlatList 不错,但 FlashList 对大型列表更好 | @shopify/flash-list —— 可直接替换,采用回收架构 |
| 列表中的大图片 | 在主线程解码全分辨率图片 | 使用带占位符 + 过渡效果的 expo-image,并指定尺寸 |
每个 FlatList 都应该具备:
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // 已记忆化的组件
getItemLayout={getItemLayout} // 如果项目高度固定
initialNumToRender={10} // 不要在挂载时渲染 100 个项目
maxToRenderPerBatch={10} // 离屏渲染的批处理大小
windowSize={5} // 在内存中保留多少个屏幕
removeClippedSubviews={true} // 卸载离屏项目 (Android)
/>
原生动画在 UI 线程上运行。JS 动画会阻塞 JS 线程并导致卡顿。
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 使用 Animated API 处理复杂动画 | Animated 在 JS 线程上运行,会阻塞交互 | 使用 react-native-reanimated —— 在 UI 线程上运行 |
| 布局动画 | 项目出现/消失时没有过渡效果 | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) |
| 共享元素过渡 | 在屏幕间导航时,元素会瞬移 | react-native-reanimated 共享过渡或 expo-router 共享元素 |
| 手势 + 动画 | 拖拽/滑动感觉有延迟 | react-native-gesture-handler + reanimated worklets —— 全部在 UI 线程上运行 |
| 测量布局 | onLayout 触发太晚,导致闪烁 | 使用带共享值的 useAnimatedStyle 实现即时响应 |
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, style]} />
</GestureDetector>
);
}
| 模式 | 问题 | 解决方案 |
|---|---|---|
| Expo Router | 适用于 React Native 的基于文件的路由(类似 Next.js) | 使用带 _layout.tsx 文件的 app/ 目录。新 Expo 项目的首选。 |
| 堆栈中的重型屏幕 | 每个屏幕都保持挂载在堆栈中 | 对于不需要持久化的屏幕,使用 unmountOnBlur: true |
| 深度链接 | 应用不响应 URL | Expo Router 会自动处理。对于裸 React Native:配置 Linking API |
| 标签页徽章更新 | 当标签页获得焦点时,徽章计数不更新 | 使用 useIsFocused() 或在获得焦点时重新获取:useFocusEffect(useCallback(...)) |
| 导航状态持久化 | 应用在后台/被杀后丢失位置 | onStateChange + initialState 配合 AsyncStorage |
app/
├── _layout.tsx # 根布局(标签导航器)
├── index.tsx # 首页标签页
├── (tabs)/
│ ├── _layout.tsx # 标签栏配置
│ ├── home.tsx
│ ├── search.tsx
│ └── profile.tsx
├── [id].tsx # 动态路由
└── modal.tsx # 模态路由
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 安全区域 | 内容被刘海屏或主屏幕指示器遮挡 | 使用 <SafeAreaView> 或 react-native-safe-area-context 中的 useSafeAreaInsets() |
| 键盘避让 | 表单字段被键盘遮挡 | <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> |
| 平台特定代码 | iOS 和 Android 需要不同的行为 | Platform.select({ ios: ..., android: ... }) 或 .ios.tsx / .android.tsx 文件 |
| 状态栏 | 状态栏与内容重叠或颜色错误 | 在根布局中使用 expo-status-bar 的 <StatusBar style="auto" /> |
| 触摸目标 | 按钮太小难以点击 | 最小 44x44 点。使用 hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} |
| 触觉反馈 | 点击感觉死板 | expo-haptics —— 在重要操作上使用 Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) |
| 模式 | 问题 | 解决方案 |
|---|---|---|
| Image 组件 | react-native 的 <Image> 功能基础 | 使用 expo-image —— 缓存、占位符、过渡效果、blurhash |
| 没有尺寸的远程图片 | 图片加载时布局发生偏移 | 始终指定 width 和 height,或使用 aspectRatio |
| 大图片 | Android 上内存不足崩溃 | 在服务器端调整大小或使用能处理内存的 expo-image |
| SVG | 不支持原生 SVG | 使用 react-native-svg + react-native-svg-transformer 来导入 SVG |
| 视频 | 视频播放 | expo-av 或 expo-video(较新的 API) |
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 使用 AsyncStorage 存储复杂数据 | 每次读取都要进行 JSON 解析/序列化 | 使用 MMKV (react-native-mmkv) —— 比 AsyncStorage 快 30 倍 |
| 全局状态 | 对于简单状态使用 Redux/MobX 显得繁琐 | Zustand —— 极简,与 React Native 配合良好 |
| 服务器状态 | 手动处理获取 + 加载 + 错误 + 缓存 | TanStack Query —— 与 Web 端相同,适用于 React Native |
| 离线优先 | 没有网络时应用无法使用 | TanStack Query persistQueryClient + MMKV,或对于复杂离线场景使用 WatermelonDB |
| 深层状态更新 | 嵌套对象使用扩展运算符非常麻烦 | 通过 Zustand 使用 Immer:set(produce(state => { state.user.name = 'new' })) |
| 模式 | 时机 | 方法 |
|---|---|---|
| 开发构建 | 需要原生模块 | npx expo run:ios 或 eas build --profile development |
| Expo Go | 快速原型设计,无需原生模块 | npx expo start —— 扫描二维码 |
| EAS Build | CI/CD,应用商店构建 | eas build --platform ios --profile production |
| EAS Update | 无需应用商店审核的热修复 | eas update --branch production --message "Fix bug" |
| 配置插件 | 无需弹出即可修改原生配置 | app.config.ts 配合 expo-build-properties 或自定义配置插件 |
| 环境变量 | 每个构建使用不同的配置 | eas.json 构建配置文件 + expo-constants |
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context
| 工具 | 用途 | 设置 |
|---|---|---|
| Jest | 单元测试,钩子测试 | Expo 默认包含 |
| React Native Testing Library | 组件测试 | @testing-library/react-native |
| Detox | 在真实设备/模拟器上进行 E2E 测试 | detox —— Wix 的测试框架 |
| Maestro | 使用 YAML 流程的 E2E 测试 | maestro test flow.yaml —— 比 Detox 更简单 |
| 陷阱 | 解决方案 |
|---|---|
| Metro 打包器缓存 | npx expo start --clear |
| Pod 安装问题 (iOS) | cd ios && pod install --repo-update |
| Reanimated 不工作 | 必须是第一个导入项:在根文件中 import 'react-native-reanimated' |
| Expo SDK 升级 | 更新 SDK 版本后执行 npx expo install --fix |
| Android 构建失败 | 检查 gradle.properties 的内存设置:org.gradle.jvmargs=-Xmx4g |
| iOS 模拟器慢 | 使用物理设备进行性能测试 —— 模拟器不能反映真实性能 |
每周安装次数
119
代码仓库
GitHub 星标数
650
首次出现
7 天前
安全审计
安装于
opencode117
gemini-cli116
warp116
amp116
cline116
kimi-cli116
Performance and architecture patterns for React Native + Expo apps. Rules ranked by impact — fix CRITICAL before touching MEDIUM.
This is a starting point. The skill will grow as you build more mobile apps.
Lists are the #1 performance issue in React Native. A janky scroll kills the entire app experience.
| Pattern | Problem | Fix |
|---|---|---|
| ScrollView for data | <ScrollView> renders all items at once | Use <FlatList> or <FlashList> — virtualised, only renders visible items |
| Missing keyExtractor | FlatList without keyExtractor → unnecessary re-renders | keyExtractor={(item) => item.id} — stable unique key per item |
| Complex renderItem | Expensive component in renderItem re-renders on every scroll | Wrap in React.memo, extract to separate component |
| Inline functions in renderItem | renderItem={({ item }) => <Row onPress={() => nav(item.id)} />} | Extract handler: const handlePress = useCallback(...) |
| No getItemLayout | FlatList measures every item on scroll (expensive) | Provide getItemLayout for fixed-height items: (data, index) => ({ length: 80, offset: 80 * index, index }) |
| FlashList | FlatList is good, FlashList is better for large lists | @shopify/flash-list — drop-in replacement, recycling architecture |
| Large images in lists | Full-res images decoded on main thread | Use expo-image with placeholder + transition, specify dimensions |
Every FlatList should have:
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // Memoised component
getItemLayout={getItemLayout} // If items are fixed height
initialNumToRender={10} // Don't render 100 items on mount
maxToRenderPerBatch={10} // Batch size for off-screen rendering
windowSize={5} // How many screens to keep in memory
removeClippedSubviews={true} // Unmount off-screen items (Android)
/>
Native animations run on the UI thread. JS animations block the JS thread and cause jank.
| Pattern | Problem | Fix |
|---|---|---|
| Animated API for complex animations | Animated runs on JS thread, blocks interactions | Use react-native-reanimated — runs on UI thread |
| Layout animation | Item appears/disappears with no transition | LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) |
| Shared element transitions | Navigate between screens, element teleports | react-native-reanimated shared transitions or expo-router shared elements |
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function AnimatedBox() {
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateX: withSpring(offset.value) }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.box, style]} />
</GestureDetector>
);
}
| Pattern | Problem | Fix |
|---|---|---|
| Expo Router | File-based routing (like Next.js) for React Native | app/ directory with _layout.tsx files. Preferred for new Expo projects. |
| Heavy screens on stack | Every screen stays mounted in the stack | Use unmountOnBlur: true for screens that don't need to persist |
| Deep linking | App doesn't respond to URLs | Expo Router handles this automatically. For bare RN: Linking API config |
| Tab badge updates | Badge count doesn't update when tab is focused | Use or refetch on focus: |
app/
├── _layout.tsx # Root layout (tab navigator)
├── index.tsx # Home tab
├── (tabs)/
│ ├── _layout.tsx # Tab bar config
│ ├── home.tsx
│ ├── search.tsx
│ └── profile.tsx
├── [id].tsx # Dynamic route
└── modal.tsx # Modal route
| Pattern | Problem | Fix |
|---|---|---|
| Safe area | Content under notch or home indicator | <SafeAreaView> or useSafeAreaInsets() from react-native-safe-area-context |
| Keyboard avoidance | Form fields hidden behind keyboard | <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> |
| Platform-specific code | iOS and Android need different behaviour | Platform.select({ ios: ..., android: ... }) or / files |
| Pattern | Problem | Fix |
|---|---|---|
| Image component | <Image> from react-native is basic | Use expo-image — caching, placeholder, transition, blurhash |
| Remote images without dimensions | Layout shift when image loads | Always specify width and height, or use aspectRatio |
| Large images | OOM crashes on Android | Resize server-side or use expo-image which handles memory |
| Pattern | Problem | Fix |
|---|---|---|
| AsyncStorage for complex data | JSON parse/stringify on every read | Use MMKV (react-native-mmkv) — 30x faster than AsyncStorage |
| Global state | Redux/MobX boilerplate for simple state | Zustand — minimal, works great with React Native |
| Server state | Manual fetch + loading + error + cache | TanStack Query — same as web, works in React Native |
| Offline first | App unusable without network | TanStack Query persistQueryClient + MMKV, or WatermelonDB for complex offline |
| Deep state updates | Spread operator hell for nested objects | Immer via Zustand: set(produce(state => { state.user.name = 'new' })) |
| Pattern | When | How |
|---|---|---|
| Development build | Need native modules | npx expo run:ios or eas build --profile development |
| Expo Go | Quick prototyping, no native modules | npx expo start — scan QR code |
| EAS Build | CI/CD, app store builds | eas build --platform ios --profile production |
| EAS Update | Hot fix without app store review | eas update --branch production --message "Fix bug" |
npx create-expo-app my-app --template tabs
cd my-app
npx expo install expo-image react-native-reanimated react-native-gesture-handler react-native-safe-area-context
| Tool | For | Setup |
|---|---|---|
| Jest | Unit tests, hook tests | Included with Expo by default |
| React Native Testing Library | Component tests | @testing-library/react-native |
| Detox | E2E tests on real devices/simulators | detox — Wix's testing framework |
| Maestro | E2E with YAML flows | maestro test flow.yaml — simpler than Detox |
| Gotcha | Fix |
|---|---|
| Metro bundler cache | npx expo start --clear |
| Pod install issues (iOS) | cd ios && pod install --repo-update |
| Reanimated not working | Must be first import: import 'react-native-reanimated' in root |
| Expo SDK upgrade | npx expo install --fix after updating SDK version |
| Android build fails | Check gradle.properties for memory: org.gradle.jvmargs=-Xmx4g |
Weekly Installs
119
Repository
GitHub Stars
650
First Seen
7 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode117
gemini-cli116
warp116
amp116
cline116
kimi-cli116
Tailwind CSS v4 + shadcn/ui 生产级技术栈配置指南与最佳实践
2,600 周安装
| Gesture + animation | Drag/swipe feels laggy | react-native-gesture-handler + reanimated worklets — all on UI thread |
| Measuring layout | onLayout fires too late, causes flash | Use useAnimatedStyle with shared values for instant response |
useIsFocused()useFocusEffect(useCallback(...))| Navigation state persistence | App loses position on background/kill | onStateChange + initialState with AsyncStorage |
.ios.tsx.android.tsx| Status bar | Status bar overlaps content or wrong colour | <StatusBar style="auto" /> from expo-status-bar in root layout |
| Touch targets | Buttons too small to tap | Minimum 44x44pt. Use hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} |
| Haptic feedback | Taps feel dead | expo-haptics — Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) on important actions |
| SVG | SVG support isn't native | react-native-svg + react-native-svg-transformer for SVG imports |
| Video | Video playback | expo-av or expo-video (newer API) |
| Config plugins | Modify native config without ejecting | app.config.ts with expo-build-properties or custom config plugin |
| Environment variables | Different configs per build | eas.json build profiles + expo-constants |
| iOS simulator slow | Use physical device for performance testing — simulator doesn't reflect real perf |