Skip to main content

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

PhaseWhereWhat happens
InjectionClient (source page)<InjectPagePrefetch> renders a <link rel="prefetch"> tag. The browser fires a low-priority background request.
PrefetchingServer (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.
ActivationClient (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.js getServerSideProps). Pages using cacheableServerProps or withTravelokaApp are 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

PropTypeDescription
hrefstringThe destination URL to prefetch. Must be a relative path starting with /.
actionTypePrefetchActionTypeThe registered key used to validate if prefetching is allowed for this flow.

Behavior

  1. Calls useEnablePagePrefetch to check all permission gates.
  2. If disabled, renders nothing (secure by default).
  3. 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
CheckSourceFailure Behavior
Global feature flaguseFeatureControl('web-rel-prefetch')Silent false
Local registrationPREFETCH_CONFIG_REGISTRATION[actionType]Logs registration_not_found warning
Remote propertyproperties[actionType] === 'ENABLE'Logs feature_control_not_found warning if undefined

Parameters

ParameterTypeDescription
prefetchActionTypePrefetchActionTypeKey from PREFETCH_CONFIG_REGISTRATION.

Returns

booleantrue 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

ParameterTypeDescription
contextGetServerSidePropsContextThe Next.js SSR context object.
featureControlFeatureControlMapThe resolved feature control map for the current request.

Returns

PrefetchStrategyResult — either a result object or null:

type PrefetchStrategyResult = {
isPrefetchRequest: true;
prefetchActionType: PrefetchActionType;
} | null;
Return valueWhen
{ isPrefetchRequest: true, prefetchActionType }Cache-Control header was successfully injected for a matching, enabled prefetch action.
nullFeature 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

  1. Guard — Returns null if the global feature flag is off or the request is not a prefetch (sec-purpose / purpose header).
  2. Init logging — Logs init_prefetching_document to record that prefetch pattern matching logic has started.
  3. URL normalization — Strips query parameters from resolvedUrl.
  4. Pattern matching — Iterates PREFETCH_CONFIG_REGISTRATION and matches the URL path against registered glob patterns. Returns null if no match.
  5. Action check — Verifies the matched action is set to 'ENABLE' in feature control properties.
  6. Header injection — Sets Cache-Control: private, max-age=<N> only if no Cache-Control header is already present.
  7. Logging & return — Records the start_prefetching_document phase and returns the result object. Returns null in 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;
PatternExample MatchDescription
/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

KeyPatternMax Age
bookingForm/booking/*60 (default)

How to Register a New Prefetchable Route

  1. Add an entry to PREFETCH_CONFIG_REGISTRATION in packages/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
},
});
  1. Configure Feature Control — Add a property with the same key (e.g., searchResults) to the web-rel-prefetch feature control, with the value 'ENABLE' or 'DISABLE'.

  2. Inject on the source page — Add <InjectPagePrefetch> on the page where the user is before they navigate:

<InjectPagePrefetch href="/flight/search/results" actionType="searchResults" />
  1. Apply on the target page — Ensure applyPrefetchStrategy is called in the target page's getServerSideProps.

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:

LevelControlEffect
Globalenabled: true/falseMaster kill-switch for all prefetching
Per-actionproperties: { 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

EventLevelPhaseWhen
Prefetch link injectedinfoinject_prefetchClient renders <link rel="prefetch">
Prefetch logic initiatedinfoinit_prefetching_documentServer detects prefetch header and begins pattern matching
Server cache header setinfostart_prefetching_documentServer matches a route and sets Cache-Control
Prefetched page activatedinfoactivate_prefetch_documentClient navigates to a previously prefetched page
Missing FC configwarnfeature_control_not_foundAction exists locally but has no remote toggle
Missing registrationwarnregistration_not_foundAction 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> — The private directive 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-Control header 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.

BrowserHeaderSupport
Chromium (Chrome, Edge, Opera)Sec-Purpose: prefetchFull support
FirefoxPurpose: prefetchFull support
SafariNo 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: private prevents sensitive prefetched content from being stored in shared caches or CDNs.
  • No header overwrite: Existing Cache-Control headers from upstream security middleware are respected.
  • Type-safe action types: PrefetchActionType is derived from the registration object, preventing typos and unregistered actions at compile time.