react-native-local-webview
v0.0.2
Published
Run CSR and Unity WebGL bundles from durable local storage without giving up their HTTPS origin.
Maintainers
Readme
react-native-local-webview
Run CSR and Unity WebGL bundles from durable local storage without giving up their HTTPS origin.
LocalWebView keeps complete static web releases in application-owned storage
and serves them at their original HTTPS URLs. It follows standard HTTP cache
semantics while adding atomic generations, rollback, and explicit storage
limits for large assets that should survive WebView-cache eviction.
Before installing
This package is designed for large CSR or Unity WebGL payloads where offline retention is worth an additional background download on first visit. An ordinary WebView remains the cheaper choice for small, always-online sites.
On iOS, preserving an HTTPS origin while returning local bytes requires private WebKit SPI. This can make an App Store submission ineligible. See iOS distribution constraint before adopting the package.
Installation
yarn add react-native-local-webview react-native-nitro-modules
cd ios && pod installRequirements:
- React Native New Architecture
- iOS 16.4+
- Android API 24+
react-native-nitro-modules
React, React Native, and Nitro are peer dependencies with * ranges.
Basic usage
import { LocalWebView } from 'react-native-local-webview';
export function Game() {
return (
<LocalWebView
source={{ uri: 'https://game.example.com/' }}
cachePolicy={{
maxBytes: 800 * 1024 * 1024,
maxGenerations: 2,
}}
onBundleStored={({ generationId }) => {
console.log('Available offline:', generationId);
}}
onBundleError={console.error}
style={{ flex: 1 }}
/>
);
}Use source={{ uri }} when migrating an existing WebView. Use virtualUrl for
a component dedicated to one entry:
<LocalWebView virtualUrl="https://game.example.com/" style={{ flex: 1 }} />Do not provide both.
Startup lifecycle
| State | Visible navigation | Durable-cache work | | ---------------------- | ---------------------------------------- | --------------------------------------------------------------------- | | First visit | Remote HTTPS immediately | Low-priority install begins ten seconds after the main document loads | | Fresh hit | Local files at the original HTTPS URLs | No network request and no payload hash | | Stale-while-revalidate | Local generation immediately | Only stale responses validate in the background | | Changed response | Current page remains unchanged | New atomic generation becomes available on the next mount | | Offline | Last HTTP-permitted generation | No origin connection required | | Failed install | Current remote/local page remains usable | Partial generation is discarded |
The ten-second delay is only for the first background installation. Cache hits have no timer: native code reads the persisted state and selects a generation before navigation without moving the asset inventory through React state.
A mounted generation is leased until its WebView unmounts. Pruning cannot remove files underneath a running page, and a local navigation failure can roll back to the previous complete generation.
Server configuration
Use ordinary browser cache headers. A practical immutable-release deployment looks like this:
# index.html: validate the release pointer, but allow instant stale startup
Cache-Control: max-age=0, stale-while-revalidate=86400, stale-if-error=604800
ETag: "index-v42"
# fingerprinted scripts and Unity artifacts
Cache-Control: max-age=31536000, immutableFresh resources produce no network request. A stale response is validated with
If-None-Match or If-Modified-Since; 304 Not Modified refreshes metadata
without reading or hashing the body. If a stale response has neither a
validator nor a usable freshness lifetime, it must be downloaded again.
The policy evaluates:
- response
max-age,no-cache,no-store,must-revalidate,immutable,stale-while-revalidate, andstale-if-error; - request
max-age,min-fresh, andmax-stale; Date,Age,Expires,ETag,Last-Modified, and legacyPragma;Varyrequest matching; and- all Fetch-compatible cache modes described below.
A required response marked no-store or Vary: * cannot be published as part
of a durable complete generation. Runtime API responses outside the collected
static graph are unaffected.
Cache modes
Set durableCacheMode without colliding with the Android WebView cacheMode
prop:
<LocalWebView durableCacheMode="no-cache" virtualUrl="https://game.example.com/" />| Value | Behavior |
| ---------------- | --------------------------------------------------------------- |
| default | Apply freshness, validators, and server stale controls |
| no-store | Bypass durable storage and use the remote source |
| reload | Bypass the stored generation and install a new one |
| no-cache | Revalidate even when the stored response is fresh |
| force-cache | Prefer a permitted stored response, including stale |
| only-if-cached | Require a permitted stored response without opening the network |
Cache limits
<LocalWebView
virtualUrl="https://game.example.com/"
cachePolicy={{
maxBytes: 800 * 1024 * 1024,
maxGenerations: 2,
maxInlineBytes: 4 * 1024 * 1024,
}}
/>| Option | Meaning | Default |
| ---------------- | ------------------------------------------------- | ------: |
| maxBytes | Accounted bytes retained for one cache root | 512 MiB |
| maxGenerations | Complete generations retained for rollback | 2 |
| maxInlineBytes | Largest parser-required resource folded into HTML | 32 MiB |
The default root is derived from the HTTPS origin in application documents
storage. Use cacheDirectory to override it.
Supported web content
The static graph collector resolves:
- classic and module scripts, including static and dynamic imports;
- stylesheets, CSS imports,
url(...),src, andsrcset; - preload and module-preload resources;
- classic and module Web Workers;
- WebAssembly URLs; and
- Unity loader, framework,
.data, and.wasmartifacts.
Parser-required bounded resources can be localized into index.html. Large
Unity, WASM, Range-read, and runtime-fetch artifacts stay as individual files
and are streamed by the platform interceptor. Runtime requests absent from the
static graph continue to the real HTTPS origin.
Origin, History API, and WebView compatibility
Local bytes are returned for their original HTTPS request URLs. The page keeps
location.origin, isSecureContext, relative URL resolution, cookies, browser
storage, CORS, same-origin checks, workers, and Range requests.
pushState, replaceState, back, forward, and go work normally and are
reported through onHistoryChange:
import { useRef } from 'react';
import { LocalWebView, type LocalWebViewHandle } from 'react-native-local-webview';
const ref = useRef<LocalWebViewHandle>(null);
<LocalWebView
ref={ref}
virtualUrl="https://app.example.com/"
onHistoryChange={({ url }) => console.log(url)}
/>;
ref.current?.goBack();The handle also implements goForward, reload, stopLoading,
injectJavaScript, postMessage, clearCache, clearHistory, clearFormData,
requestFocus, getHistoryState, and rollback.
Props, events, and imperative methods track [email protected], but
the platform views are implemented directly. POST bodies, custom entry headers,
inline HTML, and non-HTTPS sources use direct mode automatically. Direct mode is
also useful for an A/B measurement:
<LocalWebView durableCacheEnabled={false} source={{ uri: 'https://game.example.com/' }} />Events and cache APIs
onBundleReadyreports the generation selected for display.onBundleStoredreports a fully installed or revalidated generation.onDurableCacheHitreports native lookup duration and the selected URL.onCacheRollbackreports recovery to the previous generation.onBundleErrorreports cache-policy or background-installation failures.
Imperative cache helpers are exported for maintenance workflows:
import {
cacheDirectoryForOrigin,
clearLocalWebViewCache,
resolveWebBundle,
rollbackWebBundle,
} from 'react-native-local-webview';
const url = 'https://game.example.com/';
const directory = cacheDirectoryForOrigin(url);
await resolveWebBundle({ virtualUrl: url, cacheMode: 'reload' });
await rollbackWebBundle(directory);
await clearLocalWebViewCache(url);Security boundaries
Only absolute HTTPS entries are mirrored. Cross-origin static assets require an explicit allowlist:
<LocalWebView
virtualUrl="https://game.example.com/"
trustedAssetOrigins={['https://cdn.example.com']}
/>Redirect targets are checked one hop at a time, and Subresource Integrity is
enforced when present. Entry and worker CSP headers fail closed unless content
you control explicitly opts into allowContentSecurityPolicyBypass.
iOS distribution constraint
Android exposes local HTTPS response replacement through
shouldInterceptRequest. iOS has no equivalent public WKWebView API, so the
package registers an HTTPS NSURLProtocol through private WebKit SPI. Simulator
E2E verifies the runtime behavior, but private SPI can make an App Store
submission ineligible.
Known boundaries
- Data-derived runtime URLs are not installed unless they are statically discoverable; they remain ordinary network requests.
- The interceptor is not a Service Worker replacement.
- First installation duplicates response traffic outside the entry critical path.
- Simulator/emulator benchmarks do not replace physical-device validation.
Read the repository E2E guide for performance evidence and CONTRIBUTING.md for the persisted format and native execution flow.
License
MIT
