Client-Side Redirect Detection
Overview
Detects client-side redirects that add unnecessary latency to page load and impact Core Web Vitals, particularly LCP. Client-side redirects implemented via JavaScript or SPA routers can add seconds to the critical rendering path, yet they're often invisible to standard monitoring tools.
Why this matters:
Client-side redirects are one of the most impactful but hardest-to-detect performance issues:
- Add complete navigation overhead (HTML download, JS parsing, execution) just to redirect
- Can increase LCP by 3 to 6 seconds on first visit
- Waste bandwidth downloading resources that won't be displayed
- Not visible in basic performance metrics
- Often introduced accidentally during localization or A/B testing
Common patterns that trigger client-side redirects:
| Pattern | Example | Impact |
|---|---|---|
| Language detection | / loads JS → redirects to /es/ | Very high - entire page load wasted |
| A/B testing | Landing page → JS decides variant → redirects | High - experiment framework overhead |
| Authentication check | Public URL → JS checks auth → redirects to login | Moderate - blocking on auth check |
| Legacy URLs | Old URL → JS maps to new URL → redirects | High - should use 301 instead |
| SPA routing | Deep link → app loads → client router takes over | Low to moderate - depends on bundle size |
Real-world example:
User navigates to: https://example.com/
❌ What happens with client-side redirect:
• / responds with 2 KB HTML (757ms)
• Downloads main.js 3.4 MB (9,450ms)
• JS executes and redirects to /es/
• Downloads /es/ HTML and resources (2,430ms)
• Finally shows LCP image (6,320ms total)
✅ What should happen with server-side redirect:
• / responds with 301 redirect to /es/ (0-50ms)
• Browser navigates to /es/ directly
• Downloads /es/ HTML and resources
• Shows LCP image (1,200ms total)
Result: 81% improvement in LCPRedirect Flow Comparison:
Snippet
// Client-Side Redirect Detection
// https://webperf-snippets.nucliweb.net
(async () => {
const navEntries = performance.getEntriesByType('navigation');
if (navEntries.length === 0) {
console.log(
'%c⚠️ Navigation Timing not available',
'color: #f59e0b; font-weight: bold;'
);
return;
}
const navEntry = navEntries[0];
const currentURL = new URL(window.location.href);
const resources = performance.getEntriesByType('resource');
// Check for document navigations (only navigation type, exclude iframes)
// Filter out third-party iframes (analytics, ads, etc.)
const documentNavigations = resources.filter(
(r) => {
if (r.initiatorType !== 'navigation') return false;
// Only count same-origin navigations as potential redirects
try {
const resourceURL = new URL(r.name);
return resourceURL.origin === currentURL.origin;
} catch {
return false;
}
}
);
// Check for server-side redirects
const serverRedirects = navEntry.redirectCount || 0;
const redirectTime = navEntry.redirectEnd - navEntry.redirectStart;
// Detect client-side redirect patterns
const hasSPARouter = !!window.history?.state;
const historyLength = window.history.length;
// Check document.referrer for same-origin navigation
const referrer = document.referrer ? new URL(document.referrer) : null;
const sameOrigin = referrer && referrer.origin === currentURL.origin;
const referrerPath = referrer?.pathname || '';
const currentPath = currentURL.pathname;
// Detect redirect indicators
const hasRedirectParam =
currentURL.searchParams.has('redirect') ||
currentURL.searchParams.has('from') ||
currentURL.searchParams.has('origin');
console.group(
'%c🔄 Client-Side Redirect Detection',
'font-weight: bold; font-size: 14px;'
);
// Current page info
console.log('');
console.log('%c📍 Current Page:', 'font-weight: bold;');
console.log(` URL: ${currentURL.href}`);
console.log(` Path: ${currentPath}`);
if (referrer) {
console.log(` Referrer: ${referrer.href}`);
console.log(` Referrer path: ${referrerPath}`);
} else {
console.log(' Referrer: (none - direct navigation or blocked)');
}
// Server-side redirects
console.log('');
console.log('%c🌐 Server-Side Redirects:', 'font-weight: bold;');
if (serverRedirects > 0) {
console.log(`%c ⚠️ ${serverRedirects} redirect(s) detected`, 'color: #f59e0b;');
console.log(` Redirect time: ${redirectTime.toFixed(1)}ms`);
console.log(' 💡 Minimize redirect chains for better performance');
} else {
console.log('%c ✅ No server-side redirects', 'color: #22c55e;');
}
// Client-side navigation detection
console.log('');
console.log('%c📱 Client-Side Navigation Indicators:', 'font-weight: bold;');
let hasClientRedirect = false;
const indicators = [];
// Check 1: Same-origin referrer with different path (potential redirect)
if (sameOrigin && referrerPath !== currentPath && referrerPath !== '') {
hasClientRedirect = true;
indicators.push({
type: 'Same-origin navigation',
from: referrerPath,
to: currentPath,
severity: 'warning'
});
}
// Check 2: Document navigations in resource timing
if (documentNavigations.length > 0) {
hasClientRedirect = true;
documentNavigations.forEach((nav) => {
indicators.push({
type: 'Document navigation',
url: nav.name,
duration: nav.duration.toFixed(1) + 'ms',
severity: 'error'
});
});
}
// Check 3: Redirect URL parameters
if (hasRedirectParam) {
indicators.push({
type: 'Redirect parameter in URL',
params: Array.from(currentURL.searchParams.entries())
.filter(([key]) =>
key.toLowerCase().includes('redirect') ||
key.toLowerCase().includes('from') ||
key.toLowerCase().includes('origin')
)
.map(([key, val]) => `${key}=${val}`)
.join(', '),
severity: 'info'
});
}
// Check 4: SPA router state
if (hasSPARouter && historyLength > 1) {
indicators.push({
type: 'SPA router detected',
historyLength: historyLength,
state: JSON.stringify(window.history.state)?.slice(0, 100) || '{}',
severity: 'info'
});
}
// Check 5: Very fast navigation with minimal content (possible JS redirect)
const navDuration = navEntry.loadEventEnd - navEntry.startTime;
const domContentLoaded = navEntry.domContentLoadedEventEnd - navEntry.startTime;
const responseSize = navEntry.transferSize || navEntry.encodedBodySize || 0;
// Only flag if it's BOTH fast AND small (likely a redirect page)
if (sameOrigin && domContentLoaded < 500 && responseSize < 10000) {
indicators.push({
type: 'Fast minimal-content navigation',
duration: domContentLoaded.toFixed(1) + 'ms',
size: (responseSize / 1024).toFixed(1) + ' KB',
note: 'Small page that loads quickly - possible redirect page',
severity: 'warning'
});
}
if (indicators.length === 0) {
console.log('%c ✅ No client-side redirect detected', 'color: #22c55e;');
} else {
console.log(
`%c ⚠️ ${indicators.length} indicator(s) found`,
'color: #f59e0b;'
);
console.log('');
indicators.forEach((indicator, index) => {
const icon =
indicator.severity === 'error' ? '🔴' :
indicator.severity === 'warning' ? '⚠️' : 'ℹ️';
console.log(` ${icon} ${indicator.type}`);
Object.entries(indicator).forEach(([key, value]) => {
if (key !== 'type' && key !== 'severity') {
console.log(` ${key}: ${value}`);
}
});
if (index < indicators.length - 1) console.log('');
});
}
// LCP Impact Analysis
if (hasClientRedirect && documentNavigations.length > 0) {
console.log('');
console.log('%c⚡ Performance Impact:', 'font-weight: bold;');
const totalRedirectOverhead = documentNavigations.reduce(
(sum, nav) => sum + nav.duration,
0
);
console.log(` Total redirect overhead: ${totalRedirectOverhead.toFixed(0)}ms`);
if (totalRedirectOverhead > 3000) {
console.log(
'%c 🔴 CRITICAL: High impact on LCP',
'color: #ef4444; font-weight: bold;'
);
console.log(' 💡 This redirect adds significant delay to page load');
} else if (totalRedirectOverhead > 1000) {
console.log(
'%c 🟡 MODERATE: Noticeable impact on LCP',
'color: #f59e0b;'
);
} else {
console.log('%c 🟢 Low impact', 'color: #22c55e;');
}
}
// Recommendations
const hasDocumentNavigation = documentNavigations.length > 0;
const hasSameOriginRedirect = sameOrigin && referrerPath !== currentPath && referrerPath !== '';
const hasHighImpact = hasDocumentNavigation && documentNavigations.reduce((sum, nav) => sum + nav.duration, 0) > 1000;
if (hasDocumentNavigation || serverRedirects > 0) {
console.log('');
console.log('%c💡 Recommendations:', 'font-weight: bold;');
if (serverRedirects > 0) {
console.log('');
console.log(' Server-side redirects detected:');
console.log(' • Minimize redirect chains');
console.log(' • Use direct links when possible');
console.log(' • Cache redirect responses with appropriate headers');
}
if (hasDocumentNavigation) {
console.log('');
console.log(' Client-side redirect detected:');
console.log(' • Replace with server-side 301/302 redirects for better performance');
if (referrerPath && currentPath) {
console.log('');
console.log(' Example Nginx config:');
console.log(' %clocation = ' + referrerPath + ' {', 'color: #3b82f6; font-family: monospace;');
console.log(' %c return 301 ' + currentPath + ';', 'color: #3b82f6; font-family: monospace;');
console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
if (referrerPath === '/' && currentPath.match(/^\/[a-z]{2}\//)) {
console.log('');
console.log(' Or with language detection:');
console.log(' %cmap $http_accept_language $lang {', 'color: #3b82f6; font-family: monospace;');
console.log(' %c default ' + currentPath.split('/')[1] + ';', 'color: #3b82f6; font-family: monospace;');
console.log(' %c ~*^en en;', 'color: #3b82f6; font-family: monospace;');
console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
console.log(' %clocation = / {', 'color: #3b82f6; font-family: monospace;');
console.log(' %c return 301 /$lang/;', 'color: #3b82f6; font-family: monospace;');
console.log(' %c}', 'color: #3b82f6; font-family: monospace;');
}
}
} else if (hasSameOriginRedirect) {
console.log('');
console.log(' Same-origin navigation detected (no document navigation overhead):');
console.log(' • This may be SPA routing or a fast redirect');
console.log(' • If this is intentional SPA behavior, no action needed');
console.log(' • If this is a redirect, consider server-side 301/302 for consistency');
}
}
// Detailed timing breakdown
console.log('');
console.log('%c⏱️ Navigation Timing:', 'font-weight: bold;');
console.table({
'TTFB': `${(navEntry.responseStart - navEntry.startTime).toFixed(1)}ms`,
'DOM Content Loaded': `${domContentLoaded.toFixed(1)}ms`,
'Load Complete': `${navDuration.toFixed(1)}ms`,
'Redirect Time': serverRedirects > 0 ? `${redirectTime.toFixed(1)}ms` : 'N/A',
'DNS Lookup': `${(navEntry.domainLookupEnd - navEntry.domainLookupStart).toFixed(1)}ms`,
'TCP Connect': `${(navEntry.connectEnd - navEntry.connectStart).toFixed(1)}ms`,
'Request/Response': `${(navEntry.responseEnd - navEntry.requestStart).toFixed(1)}ms`,
});
// Resource timing for document navigations
if (documentNavigations.length > 0) {
console.log('');
console.log(
'%c🔴 Same-Origin Document Navigations Detected (Potential Client-Side Redirects):',
'font-weight: bold; color: #ef4444;'
);
console.log('');
console.log(
' These are same-origin navigations that suggest client-side redirects.'
);
console.log(
' If these are not expected, consider replacing with server-side redirects.'
);
console.log('');
console.table(
documentNavigations.map((nav) => ({
'Type': nav.initiatorType,
'Duration (ms)': nav.duration.toFixed(1),
'TTFB (ms)': (nav.responseStart - nav.startTime).toFixed(1),
'Transfer (KB)': nav.transferSize > 0 ? (nav.transferSize / 1024).toFixed(1) : '0',
'URL': nav.name.length > 60 ? '...' + nav.name.slice(-57) : nav.name,
}))
);
}
console.log('');
if (hasDocumentNavigation) {
console.log(
'%cℹ️ Note: Document navigations detected indicate actual client-side redirects. ' +
'For best performance, replace with server-side 301/302 redirects.',
'color: #6b7280; font-style: italic;'
);
} else if (hasSameOriginRedirect) {
console.log(
'%cℹ️ Note: Same-origin navigation detected without document navigation overhead. ' +
'This may be SPA routing or browser navigation, which is typically acceptable.',
'color: #6b7280; font-style: italic;'
);
} else {
console.log(
'%cℹ️ Note: No client-side redirects detected. ' +
'The current page was loaded directly or through server-side redirects.',
'color: #6b7280; font-style: italic;'
);
}
console.groupEnd();
})();
Understanding the Results
Current Page
Shows the current URL, path, and referrer information. If the referrer is from the same origin but a different path, this suggests a potential client-side redirect.
| Field | Description |
|---|---|
| URL | Full current URL |
| Path | Current pathname |
| Referrer | Previous page URL (if available) |
| Referrer path | Previous pathname |
No referrer can mean:
- Direct navigation (user typed URL or used bookmark)
- Navigation from HTTPS → HTTP (browsers hide referrer)
- Referrer blocked by
Referrer-Policy
Server-Side Redirects
Detects HTTP 301/302/307/308 redirects using the Navigation Timing API.
| Status | Meaning |
|---|---|
| ✅ No server-side redirects | Direct navigation, optimal |
| ⚠️ N redirect(s) detected | HTTP redirects occurred - check if necessary |
Impact:
- Each redirect adds one round-trip (50-300ms typically)
- Redirect chains (A→B→C) multiply the cost
- Mobile/slow connections amplify the impact
When redirects are acceptable:
- HTTPS enforcement (HTTP→HTTPS)
- www canonicalization (www→non-www or vice versa)
- Temporary redirects for maintenance (302)
When to avoid:
- Chains of 3+ redirects
- Client-side redirects that could be server-side
- Redirects on every page load
Client-Side Navigation Indicators
The snippet checks multiple signals to detect client-side redirects:
| Indicator | What it detects | Severity |
|---|---|---|
| Same-origin navigation | Referrer from same domain, different path | ⚠️ Warning |
| Document navigation | Same-origin page loads detected in Resource Timing (excludes third-party iframes) | 🔴 Error |
| Redirect parameter | URL contains ?redirect=, ?from=, etc. | ℹ️ Info |
| SPA router detected | History API state present | ℹ️ Info |
| Fast minimal-content navigation | Small page (<10KB) loaded quickly (<500ms) - likely a redirect page | ⚠️ Warning |
Severity levels:
- 🔴 Error: High-confidence client-side redirect detection - fix immediately
- ⚠️ Warning: Likely redirect pattern - investigate
- ℹ️ Info: Supporting evidence - may be normal behavior
Example output:
📱 Client-Side Navigation Indicators:
⚠️ 2 indicator(s) found
🔴 Document navigation
url: https://example.com/es/
duration: 2430.5ms
⚠️ Same-origin navigation
from: /
to: /es/Performance Impact
When client-side redirects are detected, the snippet estimates their impact on LCP:
| Impact Level | Overhead | Rating | Action |
|---|---|---|---|
| CRITICAL | > 3000ms | 🔴 | Fix immediately - major LCP issue |
| MODERATE | 1000-3000ms | 🟡 | High priority - noticeable delay |
| LOW | < 1000ms | 🟢 | Monitor - may be acceptable for SPAs |
Calculating overhead:
The overhead is the total duration of all document navigations detected in the Resource Timing API. This represents the additional time spent loading and processing pages that were only needed to redirect the user.
Recommendations
The snippet provides actionable recommendations based on the redirect type:
For server-side redirects:
- Minimize redirect chains
- Use direct links in your HTML/marketing campaigns
- Cache redirect responses
For client-side redirects:
- Replace with HTTP 301 (permanent) or 302 (temporary)
- Use language detection at the server/CDN level
- Implement redirects in Nginx, Apache, or CDN config
Example server-side redirect configurations:
See your web server documentation for implementing 301/302 redirects with language detection or path mapping.
Navigation Timing Table
Provides a complete breakdown of the current page load:
| Metric | Description |
|---|---|
| TTFB | Time to First Byte - server response time |
| DOM Content Loaded | When HTML is parsed and DOM is ready |
| Load Complete | When all resources finish loading |
| Redirect Time | Time spent following redirects (if any) |
| DNS Lookup | Domain name resolution time |
| TCP Connect | Connection establishment time |
| Request/Response | Time to receive the response |
Document Navigations Table
If same-origin document navigations are detected, shows detailed timing for each. These are potential client-side redirects.
Note: Third-party iframes (analytics, ads, etc.) are automatically excluded from this analysis.
| Column | Description |
|---|---|
| Type | navigation (same-origin page navigations) |
| Duration | Total time from start to load complete |
| TTFB | Time to first byte for this navigation |
| Transfer | Bytes transferred over the network |
| URL | The navigation URL (same origin as current page) |
How to Use This Snippet
1. Detect redirects on landing pages:
Navigate to your site's homepage or common landing pages and run the snippet. Look for:
- Same-origin navigation indicators
- Document navigation entries
- High performance impact
2. Compare direct vs. redirect navigation:
Test 1: Navigate directly to /es/
→ Run snippet → Note timing
Test 2: Navigate to / (which redirects to /es/)
→ Run snippet → Note timing
Compare: Is Test 2 significantly slower?3. Test in different scenarios:
- First visit (hard reload: Cmd+Shift+R)
- Repeat visit (normal reload: Cmd+R)
- With Service Worker active
- Different language preferences
- Different geographic locations
4. Validate after fixes:
After implementing server-side redirects:
- Retest with the snippet
- Verify no document navigation entries
- Confirm server-side redirects show in the "Server-Side Redirects" section
- Check that redirect time is minimal (under 50ms)
Browser Support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| Navigation Timing | 57 | 12 | 58 | 11 |
| Resource Timing | 43 | 12 | 40 | 11 |
| document.referrer | All | All | All | All |
The snippet works in all modern browsers. Older browsers may show reduced information.
Limitations
Cannot detect:
- Meta refresh redirects (
<meta http-equiv="refresh">) - Redirects that happen before the page loads
- Some SPA router navigations that don't create resource entries
- History.pushState/replaceState navigations without full page loads
Automatically filtered out:
- Third-party iframes (analytics, ads, social widgets)
- Cross-origin navigations
- Standard iframe embeds
False positives:
- SPAs that legitimately use client-side routing
- Preview/staging environments with redirect parameters
- A/B testing platforms that use URL parameters
Accuracy notes:
- Performance impact is estimated based on detected navigation entries
- Actual LCP impact may vary based on resource loading patterns
- Some redirect patterns may not leave traces in Performance APIs
Further Reading
- Avoid page redirects (opens in a new tab) | Chrome Developers
- Navigation Timing API (opens in a new tab) | MDN
- Resource Timing API (opens in a new tab) | MDN
- Optimize LCP (opens in a new tab) | web.dev
- TTFB | Measure server response time affected by redirects
- LCP Sub-Parts | See how redirects impact LCP phases