Nitro Fetch: The Drop-In Fetch That Leaves React Native's Built-In One in the Dust
The Gray Cat
React Native ships with its own fetch, and for years that has been good enough. But "good enough" is built on a relatively dated networking layer that funnels requests through the JS bridge, adding latency and missing out on the modern HTTP features web browsers have enjoyed for ages. Nitro Fetch (react-native-nitro-fetch) is a super-fast, Web-standard drop-in replacement that fixes this without asking you to rewrite a single line of your data-fetching logic. It comes from Margelo, the team behind heavy-hitters like Vision Camera and MMKV, and is built on their Nitro Modules framework for type-safe, zero-bridge native bindings.
The pitch is simple: keep the exact same fetch API you already know, but route it through a native HTTP stack (Cronet on Android, URLSession on iOS) that supports HTTP/2, HTTP/3 over QUIC, Brotli compression, disk caching, prefetching, streaming, and off-thread JSON parsing. In practice that means 15 to 25 percent faster raw requests, and dramatically faster perceived performance when you lean into prefetching. If your app spends real time waiting on the network, this is one of the highest-leverage swaps you can make.
Why It Is More Than Just "Faster Fetch"
The headline is speed, but the feature list is what makes Nitro Fetch interesting. Because it talks to a real native networking engine instead of the legacy bridge, it unlocks capabilities the built-in fetch simply cannot offer.
- True drop-in API. Same
fetch()surface, sameRequest/Responsesemantics, sameAbortControllersupport. You can often migrate by changing one import line. - Modern protocols. HTTP/1, HTTP/2, and HTTP/3 over QUIC, plus Brotli compression and disk caching out of the box.
- Prefetching. Warm up requests in-session, or even persist them across app launches so data is ready before a screen mounts.
- Streaming bodies. Read responses chunk-by-chunk with
res.body.getReader(), perfect for LLM token streams and progress UIs. - Off-thread JSON parsing. Parse and transform big payloads on a worklet thread so your JS thread never janks.
- Multipart uploads. Full
FormDatasupport including file uploads. - Native token refresh. Register a secure, cold-start auth-refresh routine backed by platform secure storage.
Under the hood, the magic comes from Nitro Modules. Rather than serializing data across the old bridge, Nitro generates JSI bindings directly from TypeScript specs, enabling synchronous calls and typed objects with near-zero overhead. It is the same foundation trusted by some of the most popular performance libraries in the ecosystem, which is a good signal that the plumbing here is mature.
Getting It Into Your Project
Nitro Fetch is a Nitro Module, so it has one required peer dependency: react-native-nitro-modules. You install both together.
npm install react-native-nitro-fetch react-native-nitro-modules
yarn add react-native-nitro-fetch react-native-nitro-modules
A few requirements to keep in mind. Nitro Fetch targets the New Architecture and needs React Native 0.75 or newer (both are Nitro Modules requirements). Because it ships native code, you will need to rebuild your app after installing: run pod install on iOS, then build with npx react-native run-ios or run-android. This is a native library, so it does not work in Expo Go. If you are on Expo, you will need a development build via prebuild and the dev client.
There are also a couple of optional companion packages worth noting: react-native-worklets (only needed for off-thread worklet mapping), react-native-nitro-text-decoder (for decoding streamed chunks), and react-native-nitro-websockets (a separate WebSocket package with prewarming). You only pull these in when you actually use the corresponding feature.
The Everyday Swap
The whole point of Nitro Fetch is that you barely have to change anything. Import fetch from the package and use it exactly as you always have.
A Request That Looks Familiar
import { fetch } from 'react-native-nitro-fetch'
async function loadProfile() {
const res = await fetch('https://httpbin.org/get')
const json = await res.json()
return json
}
That is the entire migration for most call sites. The Response object behaves like the standard one, so res.json(), res.text(), res.headers.get(...), and res.status all work as expected. If you would rather be explicit about which fetch you are calling, alias it on import with import { fetch as nitroFetch } and use nitroFetch at your call sites. In many codebases you can even swap the import once at a shared boundary and let everything downstream benefit without touching a thing.
Cancelling In-Flight Requests
AbortController is fully supported, so the cancellation patterns you already use keep working. This matters for screens that unmount mid-request or searches that fire on every keystroke.
import { fetch } from 'react-native-nitro-fetch'
const controller = new AbortController()
setTimeout(() => controller.abort(), 500)
try {
const res = await fetch('https://httpbin.org/delay/20', {
signal: controller.signal,
})
await res.json()
} catch (e: any) {
if (e.name === 'AbortError') {
console.log('Request was cancelled')
}
}
One nice detail: if you pass a signal that is already aborted, Nitro Fetch throws immediately without making a network call at all, which saves you a wasted round trip.
Uploading Files With FormData
Multipart uploads work the way you expect, including attaching a local file with the standard { uri, type, name } shape that React Native developers know well.
import { fetch } from 'react-native-nitro-fetch'
const fd = new FormData()
fd.append('username', 'nitro_user')
fd.append('avatar', {
uri: 'file:///path/to/photo.jpg',
type: 'image/jpeg',
name: 'avatar.jpg',
} as any)
const res = await fetch('https://httpbin.org/post', {
method: 'POST',
body: fd,
})
const json = await res.json()
Pushing the Performance Envelope
Once the basics are in place, the features that genuinely move the needle on perceived speed come into play. This is where Nitro Fetch pulls ahead of anything you can do with the built-in transport.
Prefetching Before the User Asks
The single most impactful trick is prefetching. You can warm a request in-session, and a later fetch with a matching prefetchKey resolves straight from cache.
import { prefetch, fetch } from 'react-native-nitro-fetch'
await prefetch('https://httpbin.org/uuid', {
headers: { prefetchKey: 'uuid' },
})
// A later fetch with the same prefetchKey resolves from the warmed cache
const res = await fetch('https://httpbin.org/uuid', {
headers: { prefetchKey: 'uuid' },
})
console.log('served from prefetch:', res.headers.get('nitroPrefetched'))
Even better, you can persist prefetches across launches with prefetchOnAppStart. Register the request once, and on the next cold start the native layer fires it early so the data is sitting in cache before your screen even mounts.
import { prefetchOnAppStart, fetch } from 'react-native-nitro-fetch'
await prefetchOnAppStart('https://httpbin.org/uuid', {
prefetchKey: 'uuid',
})
// On the next app launch, the matching fetch may resolve instantly
This is where the raw 20-percent request speedup turns into something users actually feel. The README cites time-to-interactive improvements of around 220 milliseconds when data is prefetched on startup and ready before the screen renders. For a launch-critical screen, that is the difference between a spinner and an instant load.
Streaming Responses Chunk by Chunk
For LLM token streams, server-sent events, or just very large responses, Nitro Fetch supports true streaming bodies. Pass stream: true and read from res.body.getReader().
import { useRef, useState } from 'react'
import { fetch as nitroFetch } from 'react-native-nitro-fetch'
import { TextDecoder } from 'react-native-nitro-text-decoder'
export function StreamingExample() {
const [output, setOutput] = useState('')
const decoder = useRef(new TextDecoder())
const runStream = async () => {
const res = await nitroFetch('https://httpbin.org/stream/20', {
stream: true,
})
const reader = res.body?.getReader()
if (!reader) return
while (true) {
const { done, value } = await reader.read()
if (done) break
setOutput((prev) => prev + decoder.current.decode(value, { stream: true }))
}
}
// ...render output and a button that calls runStream
}
This pattern shines for AI chat interfaces where you want tokens to appear as they arrive, rather than waiting for the whole response. The TextDecoder comes from the companion react-native-nitro-text-decoder package, which decodes the raw byte chunks into text.
Parsing Big Payloads Off the JS Thread
Large JSON responses can block the JS thread while they parse, causing dropped frames right when your UI needs to update. Nitro Fetch can offload that work to a worklet thread with nitroFetchOnWorklet, so parsing and mapping happen entirely off the main thread.
import { nitroFetchOnWorklet } from 'react-native-nitro-fetch'
const data = await nitroFetchOnWorklet(
'https://httpbin.org/get',
undefined,
(payload) => {
'worklet'
return JSON.parse(payload.bodyString ?? '{}')
},
)
The mapping function runs as a worklet, so you can do heavy transformation right there and only hand the finished result back to your JS thread. This requires react-native-worklets as an optional dependency.
Refreshing Auth Tokens at Cold Start
Auth handling on cold start is a perennial headache, especially when prefetches need a valid token before any of your JS even runs. Nitro Fetch can register a native auth-refresh routine that runs before prefetches and requests, stores tokens in platform secure storage (Android Keystore and encrypted SharedPreferences, iOS Keychain), and injects them as headers automatically.
import { registerTokenRefresh } from 'react-native-nitro-fetch'
registerTokenRefresh({
target: 'fetch', // 'fetch' | 'websocket' | 'all'
url: 'https://api.example.com/oauth/token',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'client_credentials' }),
responseType: 'json',
mappings: [
{
jsonPath: 'access_token',
header: 'Authorization',
valueTemplate: 'Bearer {{value}}',
},
],
onFailure: 'useStoredHeaders', // or 'skip'
})
It supports mapping JSON fields to headers via dot paths, composite multi-path header templates, injecting values into prefetch request bodies, and modifying multipart fields. There are helpers for calling the refresh endpoint manually, clearing config, and inspecting stored config too. The result is that your prefetched, cold-start requests can be authenticated correctly without any JS-side token juggling.
A Few Honest Caveats
Nitro Fetch is excellent, but it is worth being clear-eyed about the trade-offs. It is a native library, so it needs a custom dev build and will not run in Expo Go. It requires the New Architecture and React Native 0.75 or newer, which is the main adoption gate. Some features (WebSockets, TextDecoder, worklet mapping) live in separate companion packages you opt into. And it is a young, fast-moving project: it crossed 1.0 and is already on the 1.4.x line with releases landing roughly weekly. That pace is great for momentum but means you should pin versions in production and skim the changelog before upgrading.
It is also fair to note that the published benchmarks come from the authors, and real-world gains vary by network, payload, and device. The biggest, most reliable wins come from prefetching and perceived performance rather than the raw per-request speedup.
The Verdict
Nitro Fetch occupies a genuinely unique spot in the React Native ecosystem. Tools like axios, TanStack Query, and SWR live above the transport and ultimately ride on the same slow underlying networking. Nitro Fetch upgrades the transport itself, which means it is complementary to your existing data layer rather than competitive with it. You can keep your caching library, keep your hooks, and just give them a faster engine underneath.
If you are already on the New Architecture and your app does meaningful networking, the value proposition is hard to argue with: a near-zero-effort, standards-compatible import swap that makes every request faster, plus a prefetching story that can make screens feel instant. For perceived performance per line of code changed, few upgrades come close.