Page Prefetch
Speculative prefetching of full HTML documents so that subsequent navigations feel instant. The module coordinates a client-side injection phase (renders <link rel="prefetch"> in the <head>) with a server-side strategy phase (detects prefetch requests and stamps the response with Cache-Control).
How It Works
┌─────────────────────────────────────┐
│ SOURCE PAGE │
│ │
│ <InjectPagePrefetch │
│ href="/booking/123" │
│ actionType="bookingForm" /> │
│ │
│ 1. useEnablePagePrefetch checks: │
│ ├─ Global FC flag enabled? │
│ ├─ Action registered locally? │
│ └─ Remote property = "ENABLE"? │
│ │
│ 2. Renders: │
│ <link rel="prefetch" │
│ href="/booking/123" │
│ as="document" /> │
│ │
│ 3. Logs: inject_prefetch │
└──────────────┬──────────────────────┘
│ Browser fires background GET
│ with header: Sec-Purpose: prefetch
▼
┌─────────────────────────────────────┐
│ TARGET PAGE (SSR) │
│ │
│ applyPrefetchStrategy(): │
│ 1. Detects sec-purpose / purpose │
│ header = "prefetch" │
│ 2. Logs: init_prefetching_document │
│ 3. Matches URL against registered │
│ patterns (glob → regex) │
│ 4. Sets response header: │
│ Cache-Control: private, │
│ max-age=60 │
│ 5. Logs: start_prefetching_document│
│ 6. Returns PrefetchStrategyResult │
└──────────────┬──────────────────────┘
│ Browser caches the full HTML
▼
┌─────────────────────────────────────┐
│ USER NAVIGATES │
│ Browser serves cached document │
│ → Instant page load │
└─────────────────────────────────────┘
Quick Start Integration
As a consumer of this module, you do not need to worry about the server-side cache implementation. To enable prefetching for a new route, simply complete these three steps:
1. Configure Feature Control
Prefetching is gated by the web-rel-prefetch feature control. Add your new action to the remote properties to enable it.
- Key:
web-rel-prefetch - Property: Your action name (e.g.,
searchResults) - Value:
'ENABLE'
2. Register the Action
Add your new route to PREFETCH_CONFIG_REGISTRATION inside packages/core/prefetch/action-registration.ts.
import { definePrefetchConfig } from './action-registration';
export const PREFETCH_CONFIG_REGISTRATION = definePrefetchConfig({
// Existing routes...
bookingForm: {
pattern: '/booking/*',
},
// Add your new route here:
searchResults: {
pattern: '/flight/search/*', // Glob-style pattern matching the target URL
maxAge: 120, // Optional: cache duration in seconds (defaults to 60s)
},
});
3. Inject the Component
On the source page (the page the user is currently viewing, before they navigate), render the <InjectPagePrefetch /> component.
import { InjectPagePrefetch } from '@traveloka/core';
function FlightListing() {
const nextUrl = `/flight/search/results?id=123`;
return (
<>
{/* Prefetch the next page while the user browses this one */}
<InjectPagePrefetch href={nextUrl} actionType="searchResults" />
{/* ... rest of the page */}
</>
);
}
Lifecycle Phases
| Phase | Where | What happens |
|---|---|---|
| Injection | Client (source page) | <InjectPagePrefetch> renders a <link rel="prefetch"> tag. The browser fires a low-priority background request. |
| Prefetching | Server (target page) | applyPrefetchStrategy detects the prefetch header, matches the URL to a registration entry, sets Cache-Control: private, max-age=<N>, and returns a PrefetchStrategyResult. |
| Activation | Client (navigation) | The user clicks a link. The browser serves the cached HTML response instantly instead of making a new network request. Tracked via logActivationPrefetch. |
Limitations
- Server-side getServerSideProps only: Prefetch detection is only available within
getSharedServerProps(our wrapper around Next.jsgetServerSideProps). Pages usingcacheableServerPropsorwithTravelokaAppare not supported.
Module Structure
packages/core/prefetch/
├── index.ts # Public exports
├── constants.ts # Feature control key & metric name
├── action-registration.ts # Route registry & types
├── prefetch-utils.ts # Header detection & glob matching
├── prefetch-logger.ts # Observability helpers
├── clients/
│ ├── InjectPagePrefetch.tsx # Client component (renders <link>)
│ └── useEnablePagePrefetch.ts # Client hook (permission check)
└── server/
└── apply-prefetch-strategy.ts # Server-side cache header logic
Public API
All exports are available from @traveloka/core:
import {
InjectPagePrefetch,
useEnablePagePrefetch,
applyPrefetchStrategy,
isPrefetchingPage,
matchPath,
} from '@traveloka/core';
Client-Side
<InjectPagePrefetch>
A declarative React component that injects a <link rel="prefetch"> tag into the HTML <head> via Next.js Head.
Props
| Prop | Type | Description |
|---|---|---|
href | string | The destination URL to prefetch. Must be a relative path starting with /. |
actionType | PrefetchActionType | The registered key used to validate if prefetching is allowed for this flow. |
Behavior
- Calls
useEnablePagePrefetchto check all permission gates. - If disabled, renders nothing (secure by default).
- If enabled, renders
<link rel="prefetch" href="..." as="document" />and logs the injection event.
Example
import { InjectPagePrefetch } from '@traveloka/core';
function FlightSearchResults() {
const bookingUrl = `/booking/${flightId}`;
return (
<>
{/* Prefetch the booking page while user browses results */}
<InjectPagePrefetch href={bookingUrl} actionType="bookingForm" />
{/* ... rest of the page */}
</>
);
}
useEnablePagePrefetch(actionType)
A React hook that performs a three-layer permission check to determine if prefetching is allowed.
Validation Flow
Feature Control enabled? ─── No ──→ return false
│ Yes
▼
Action registered locally? ─── No ──→ log warning + return false
│ Yes
▼
Remote property = "ENABLE"? ─── No ──→ log warning (if undefined) + return false
│ Yes
▼
return true
| Check | Source | Failure Behavior |
|---|---|---|
| Global feature flag | useFeatureControl('web-rel-prefetch') | Silent false |
| Local registration | PREFETCH_CONFIG_REGISTRATION[actionType] | Logs registration_not_found warning |
| Remote property | properties[actionType] === 'ENABLE' | Logs feature_control_not_found warning if undefined |
Parameters
| Parameter | Type | Description |
|---|---|---|
prefetchActionType | PrefetchActionType | Key from PREFETCH_CONFIG_REGISTRATION. |
Returns
boolean — true only when all three checks pass.
Server-Side
applyPrefetchStrategy({ context, featureControl })
Server-side function invoked during getServerSideProps. It detects prefetch requests and injects a Cache-Control header so the browser caches the rendered HTML.
Parameters
| Parameter | Type | Description |
|---|---|---|
context | GetServerSidePropsContext | The Next.js SSR context object. |
featureControl | FeatureControlMap | The resolved feature control map for the current request. |
Returns
PrefetchStrategyResult — either a result object or null:
type PrefetchStrategyResult = {
isPrefetchRequest: true;
prefetchActionType: PrefetchActionType;
} | null;
| Return value | When |
|---|---|
{ isPrefetchRequest: true, prefetchActionType } | Cache-Control header was successfully injected for a matching, enabled prefetch action. |
null | Feature control disabled, not a prefetch request, no pattern match, action disabled, or Cache-Control already set. |
The return value can be used by callers to determine if the current request was a successful prefetch and which action matched, enabling downstream logic such as activation logging.
Logic
- Guard — Returns
nullif the global feature flag is off or the request is not a prefetch (sec-purpose/purposeheader). - Init logging — Logs
init_prefetching_documentto record that prefetch pattern matching logic has started. - URL normalization — Strips query parameters from
resolvedUrl. - Pattern matching — Iterates
PREFETCH_CONFIG_REGISTRATIONand matches the URL path against registered glob patterns. Returnsnullif no match. - Action check — Verifies the matched action is set to
'ENABLE'in feature control properties. - Header injection — Sets
Cache-Control: private, max-age=<N>only if noCache-Controlheader is already present. - Logging & return — Records the
start_prefetching_documentphase and returns the result object. Returnsnullin all other cases.
Example
import { applyPrefetchStrategy } from '@traveloka/core';
export const getServerSideProps = async context => {
const featureControl = await getFeatureControl();
const prefetchResult = applyPrefetchStrategy({ context, featureControl });
// prefetchResult can be used for downstream logic
// e.g., skip expensive data fetching during prefetch
// ... rest of SSR logic
return {
props: {
/* ... */
},
};
};
Utility Functions
isPrefetchingPage(context)
Detects if the current SSR request is a speculative background fetch by inspecting request headers.
function isPrefetchingPage(context: GetServerSidePropsContext): boolean;
Returns true when sec-purpose: prefetch (Chromium) or purpose: prefetch (other browsers) is present. The header is absent during actual navigation, even if the page was previously prefetched.
matchPath(pattern, urlPath)
Matches a URL path against a glob-style pattern.
function matchPath(pattern: string, urlPath: string): boolean;
| Pattern | Example Match | Description |
|---|---|---|
/booking/* | /booking/123 | * matches a single path segment |
/booking/** | /booking/123/edit/confirm | ** matches multiple path segments |
Action Registration
All prefetchable routes are registered in PREFETCH_CONFIG_REGISTRATION inside packages/core/prefetch/action-registration.ts.
PrefetchConfig
interface PrefetchConfig {
/** Glob-style path pattern (e.g., '/booking/*'). */
pattern: string;
/**
* Duration in seconds for the browser to keep the prefetched resource.
* @default 60
*/
maxAge?: number;
}
Current Registrations
| Key | Pattern | Max Age |
|---|---|---|
bookingForm | /booking/* | 60 (default) |
How to Register a New Prefetchable Route
- Add an entry to
PREFETCH_CONFIG_REGISTRATIONinpackages/core/prefetch/action-registration.ts:
export const PREFETCH_CONFIG_REGISTRATION = definePrefetchConfig({
bookingForm: {
pattern: '/booking/*',
},
// Add your new route here:
searchResults: {
pattern: '/flight/search/*',
maxAge: 120, // optional, defaults to 60s
},
});
-
Configure Feature Control — Add a property with the same key (e.g.,
searchResults) to theweb-rel-prefetchfeature control, with the value'ENABLE'or'DISABLE'. -
Inject on the source page — Add
<InjectPagePrefetch>on the page where the user is before they navigate:
<InjectPagePrefetch href="/flight/search/results" actionType="searchResults" />
- Apply on the target page — Ensure
applyPrefetchStrategyis called in the target page'sgetServerSideProps.
The PrefetchActionType union type updates automatically since it is derived from the registration object keys.
Feature Control
Prefetching is gated behind a two-level Feature Control system under the key web-rel-prefetch:
| Level | Control | Effect |
|---|---|---|
| Global | enabled: true/false | Master kill-switch for all prefetching |
| Per-action | properties: { bookingForm: 'ENABLE' } | Granular toggle per registered route |
Both levels must be active for prefetching to proceed. This allows safe rollout and instant rollback without code changes.
Observability
All logging is routed through the centralized logger under the metric name webpage.prefetch.
Log Events
| Event | Level | Phase | When |
|---|---|---|---|
| Prefetch link injected | info | inject_prefetch | Client renders <link rel="prefetch"> |
| Prefetch logic initiated | info | init_prefetching_document | Server detects prefetch header and begins pattern matching |
| Server cache header set | info | start_prefetching_document | Server matches a route and sets Cache-Control |
| Prefetched page activated | info | activate_prefetch_document | Client navigates to a previously prefetched page |
| Missing FC config | warn | feature_control_not_found | Action exists locally but has no remote toggle |
| Missing registration | warn | registration_not_found | Action type is not in PREFETCH_CONFIG_REGISTRATION |
Log Payload Structure
// inject_prefetch (client)
{ phase: 'inject_prefetch', action_type: 'bookingForm', config: { pattern: '/booking/*' } }
// init_prefetching_document (server — before pattern matching)
{ phase: 'init_prefetching_document', resolved_url: '/booking/123' }
// start_prefetching_document (server — after cache-control injection)
{ phase: 'start_prefetching_document', action_type: 'bookingForm', resolved_url: '/booking/123', config: { pattern: '/booking/*' } }
// activate_prefetch_document (client — on navigation to prefetched page)
{ phase: 'activate_prefetch_document', action_type: 'bookingForm', config: { pattern: '/booking/*' } }
Cache Behavior
Cache-Control: private, max-age=<N>— Theprivatedirective ensures the response is only cached by the end-user's browser; CDNs and shared proxies will not store it.- Default TTL: 60 seconds (configurable per route via
maxAge). - Existing headers preserved: If a
Cache-Controlheader is already set by upstream middleware, prefetch logic will not overwrite it.
Browser Compatibility
The prefetch mechanism relies on the browser's native <link rel="prefetch"> support and the Sec-Purpose / Purpose request headers.
| Browser | Header | Support |
|---|---|---|
| Chromium (Chrome, Edge, Opera) | Sec-Purpose: prefetch | Full support |
| Firefox | Purpose: prefetch | Full support |
| Safari | — | No native <link rel="prefetch"> support |
The server-side detection checks both headers for maximum compatibility.
Security Considerations
- Secure by default: If any permission check fails, the component renders nothing and no headers are set.
- Private caching only:
Cache-Control: privateprevents sensitive prefetched content from being stored in shared caches or CDNs. - No header overwrite: Existing
Cache-Controlheaders from upstream security middleware are respected. - Type-safe action types:
PrefetchActionTypeis derived from the registration object, preventing typos and unregistered actions at compile time.