Best Practices
Production recommendations for configuring, debugging, and operating resource-fallback.
Rule configuration
Align rule base with Vite base / publicPath
| Build tool | Align rule base with |
|---|---|
| Vite | Vite base |
| Webpack | output.publicPath |
If the first resource URL does not match the rule base, the runtime never enters retry/fallback. Vite's config base and FallbackRule.base share a name — keep them equal in practice.
urls order is fallback order
Recommended chain:
Primary CDN → Backup CDN → Self-hosted static origin → Same-origin '/'The last entry is usually '/' (relative origin) to avoid hitting a broken CDN again.
{
base: 'https://cdn.example.com/',
urls: [
'https://cdn-backup.example.com/',
'https://static.mysite.com/',
'/', // origin — always last
],
}Use trailing slashes on CDN prefixes
Prefix URLs should end with / (e.g. https://cdn.example.com/). The runtime uses joinAssetPrefix to avoid malformed paths like ...prod + js/foo.js → ...prodjs/foo.js.
Retry overrides and current circuit boundary
Retry settings can still be overridden per rule when different asset classes need different retry budgets:
rules: [
{
base: 'https://cdn.example.com/',
urls: ['https://cdn-backup.example.com/', '/'],
retry: { max: 2, baseDelay: 300 },
},
],
defaults: {
retry: { max: 2 },
circuit: { threshold: 5, cooldown: 30000, shareAcrossTabs: true },
},Keep retry.max between 1–3. Excessive retries increase user wait time.
Current page-side rule/circuit behavior is narrower than the public type suggests:
- compilation sorts rules by descending
baselength, so longer prefixes match first; window.__RF__.url(filename)always builds the initial URL from the first compiled rule'sbase; it is not circuit-aware;- a page recovery session first chooses one rule from that initial URL, then walks that rule's ordered
urlscandidates; - the page runtime currently creates one circuit registry, initialized from the first compiled rule's circuit options.
FallbackRule.circuitremains public, but independent per-rule page circuits are not implemented yet.
CDN prefix notes
- Same artifact on all CDNs — required for
sri: 'keep'/'strict' - CORS headers on fonts — fallback font origins need
Access-Control-Allow-Originfor cross-origin@font-face - preconnect — leave
injectPreconnect: true(default) to reduce DNS + TLS latency on fallback hosts
Debugging tips
Enable debug logging
localStorage.__RF_DEBUG__ = '1';
location.reload();Or set debug: true in config (always logs — use sparingly in production).
Verify in the right environment
| Environment | Dynamic import fallback |
|---|---|
Vite dev | ✗ Not supported |
Vite preview / production | ✓ |
| Webpack production | ✓ |
Network panel checklist
- First request to primary CDN fails
- Retries on same host (with
__rf=on module scripts) - Fallback to next URL in
urls - Final success or
rf:error
Hybrid SW debugging
- Use
localhost,127.0.0.1, or HTTPS — not LAN IP over HTTP - Clear old SW + caches after rebuild
- Verify
navigator.serviceWorker.controller?.scriptURLmatches current build
Monitoring
Treat page rf:error as a terminal signal, and use rf:retry / rf:fallback to prove that fallback actually ran:
window.addEventListener('rf:error', (e) => {
analytics.track('resource_fallback_terminal', (e as CustomEvent).detail);
});Track fallback chains:
window.addEventListener('rf:fallback', (e) => {
const detail = (e as CustomEvent).detail;
analytics.track('resource_fallback_switch', {
from: detail.from,
to: detail.to,
});
});See Runtime Events for full API.
Page-side rf:error means a terminal page recovery failure; that can be candidate exhaustion, a Coordinator deadline, or another terminal recovery error. SW-bridged rf:error may also carry resolver giveup states such as rules-exhausted or no-match.
If you need reason-string analysis, keep it explicitly scoped to SW-bridged events. Page-side rf:error.detail.reason is an opaque failure value, not a stable public contract.
Entry and lazy-route fallback UI
- Entry bundle — add
rf:errorlistener inindex.htmlbefore app scripts - Lazy routes — wrap
React.lazy/ async components with ErrorBoundary - Do not auto-reload on
rf:error— the library intentionally leaves recovery to the application
Sync script limitations
When a classic (non-module) <script> fails:
- Browser fires
erroronly — already-executed code is irreversible - The plugin replaces the DOM node and reloads, but re-execution may cause side effects if globals were partially mounted
- When all URLs are exhausted, only
rf:errorfires — no automaticlocation.reload()
Hybrid SW does not take over scripts and does not guarantee strict ordering for synchronous classic scripts. Strong ordering requires a future opt-in ScriptSequencer capability.