agentsclimarketplace

Performance

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

Ship production-grade RN libraries faster — scaffold, audit, and publish with Claude Code.

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.

What its author says it does

Copied from the file, not written here

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

SKILL.md

8.0 KB, 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]
)

Keep looking

Skills are one crate of 328,083. 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.