agentsclimarketplace

Performance

Skill Shankulkarni/RN-Lib-Claude/plugins/rn-lib-claude/skills/performance

Use when building performance-sensitive React Native library components — list virtualization, JS thread safety, bundle size, FlashList, FPS diagnosis, and Hermes-specific patterns.From its SKILL.md

Install
npx -y skills add Shankulkarni/RN-Lib-Claude --skill performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.0 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

Performance Patterns for RN Libraries

As a library author, your decisions affect every consumer app. These are RN-specific — not general React patterns.


List Virtualization

Never use ScrollView for unbounded lists. It renders all items upfront.

// ❌ ScrollView — 5,000 items = 1–3s freeze
<ScrollView>
  {countries.map(c => <CountryRow key={c.code} {...c} />)}
</ScrollView>

// ✅ FlashList — only renders visible items, ~50ms for 5,000 items
import { FlashList } from '@shopify/flash-list'

<FlashList
  data={countries}
  renderItem={({ item }) => <CountryRow code={item.code} name={item.name} />}
  estimatedItemSize={56}
  keyExtractor={item => item.code}
/>

Pass primitives to memoized list items — not objects

// ❌ Object prop — new reference every render, memo is useless
<MemoRow data={item} onPress={handlePress} />

// ✅ Primitives — shallow comparison works
<MemoRow code={item.code} name={item.name} onPress={handlePress} />

getItemType for heterogeneous lists

<FlashList
  data={feed}
  getItemType={item => item.type}  // prevents layout thrashing during recycling
  renderItem={({ item }) => { ... }}
/>

Hoist callbacks outside renderItem

// ❌ New function per render — breaks all item memoization
<FlashList renderItem={({ item }) => <Row onPress={() => select(item.id)} />} />

// ✅ Stable reference — items receive the same function instance
const handleSelect = useCallback((id: string) => select(id), [])
<FlashList renderItem={({ item }) => <Row id={item.id} onPress={handleSelect} />} />

JS Thread — Don't Block It

The JS thread drives your library. Blocking it drops frames for the consumer's entire app.

What blocks the JS thread:

  • Synchronous operations over ~4ms
  • Large JSON.parse / JSON.stringify in render
  • Unthrottled event handlers (scroll, pan)
  • Filtering/sorting large arrays during render

Patterns to avoid:

// ❌ Expensive filter on every render
function CountryPicker({ query }: { query: string }) {
  const results = allCountries.filter(c =>   // runs every render
    c.name.toLowerCase().includes(query.toLowerCase())
  )
  return <FlashList data={results} ... />
}

// ✅ Memoize expensive derivations
function CountryPicker({ query }: { query: string }) {
  const results = useMemo(
    () => allCountries.filter(c => c.name.toLowerCase().includes(query.toLowerCase())),
    [query]
  )
  return <FlashList data={results} ... />
}

For search/filter with large datasets: debounce the input, not the filter.

const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 150)  // only refilter after typing stops
const results = useMemo(
  () => allCountries.filter(c => c.name.includes(debouncedQuery)),
  [debouncedQuery]
)

FPS Targets

FPSStateAction
55–60SmoothShip it
45–55StutterInvestigate
30–45NoticeableFix before publish
<30BrokenBlock publish

Diagnose in the example app:

  • Shake device (or Cmd+D in simulator) → Show Perf Monitor
  • JS FPS drops: JS thread blocked — look for expensive renders, heavy useMemo, sync operations
  • UI FPS drops only, JS FPS fine: GPU/layout issue — look for shadows, overflow: hidden, large blurs

Bundle Size — Your Responsibility

Every byte you add gets paid by every consumer app, on every install.

No internal barrel exports

// ❌ Barrel — Metro loads everything even if consumer imports one export
// src/components/index.ts
export * from './Button'
export * from './Input'
export * from './Modal'

// ✅ Export directly from src/index.ts — only the public API
export { Button } from './components/Button'
export { Input } from './components/Input'

sideEffects: false in package.json

{ "sideEffects": false }

Tells bundlers all exports are pure — unused ones are safely tree-shaken.

Size benchmarks

Check with pkg-size.dev or bundlephobia:

Size (minified + gzip)Verdict
< 5 KBGreat
5–20 KBAcceptable — document what's included
20–50 KBJustify — what is this size coming from?
> 50 KBBlock unless it bundles a necessary data file (e.g. country database)

Never bundle what consumers already have

// ❌ Direct dep — shipped twice
"dependencies": { "react-native-reanimated": "^3.0.0" }

// ✅ Peer dep — one instance in the consumer's app
"peerDependencies": { "react-native-reanimated": ">=3.0.0" }

Hermes-Specific Patterns

All RN 0.76+ apps run on Hermes. Write Hermes-friendly code.

Avoid eval and Function() constructors — Hermes uses AOT compilation, dynamic code generation is unsupported and will throw.

Avoid arguments object — use rest params instead:

// ❌ arguments — not optimized by Hermes
function format() { return Array.from(arguments).join(', ') }

// ✅ Rest params — Hermes-optimized
function format(...args: string[]) { return args.join(', ') }

WeakRef and FinalizationRegistry are available in Hermes — use them for optional cleanup of large resources (image caches, native object references) without causing memory leaks.


React Memoization

Use these when the component receives frequent re-renders or passes callbacks to memoized children.

// memo — component whose parent re-renders often but its own props are stable
const CountryRow = memo(({ code, name, onSelect }: CountryRowProps) => (
  <Pressable onPress={() => onSelect(code)}>
    <Text>{name}</Text>
  </Pressable>
))

// useMemo — expensive derived value
const filteredCountries = useMemo(
  () => countries.filter(c => c.name.toLowerCase().includes(query)),
  [query]
)

// useCallback — stable function reference passed to memoized children
const handleSelect = useCallback((code: string) => {
  onSelect?.(code)
}, [onSelect])

Don't over-memoize — memo with object props silently breaks (fails shallow equality). Measure with the Perf Monitor before adding memoization.


React Compiler Compatibility (RN 0.76+, Expo SDK 52+)

React Compiler auto-memoizes components. Write compiler-friendly code so consumers benefit:

// ✅ Destructure function props at render top — compiler can track them
function MyComponent({ onPress, onLongPress }: Props) {
  const handlePress = () => onPress()
  const handleLongPress = () => onLongPress?.()
  return <Pressable onPress={handlePress} onLongPress={handleLongPress} />
}

// ❌ Dot-access in JSX — compiler misses the optimization
<Pressable onPress={() => props.onPress()} />

TextInput — Uncontrolled for Typing Performance

Controlled TextInput sends every keystroke through the JS bridge, which can cause flicker on fast typing. For search inputs in pickers and filters, prefer uncontrolled:

// ❌ Controlled — every keystroke round-trips through JS
<TextInput value={query} onChangeText={setQuery} />

// ✅ Uncontrolled — native manages state during typing, JS only sees final value
<TextInput
  defaultValue={initialQuery}
  onChangeText={text => {
    queryRef.current = text
    onQueryChange?.(text)
  }}
/>

Never combine value and defaultValue.


Intl / Date Formatting — Module Scope

Intl constructors are expensive. Create once at module scope, not per render.

// ❌ Recreated on every call
function formatDate(date: Date) {
  return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date)
}

// ✅ Created once
const dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' })
export function formatDate(date: Date) { return dateFormatter.format(date) }

// ✅ Or useMemo if locale is dynamic
const formatter = useMemo(
  () => new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }),
  [locale]
)

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.