SPX (Single Page XHR) is a blazing fast, lightweight (7.8kb gzipped) and feature full new generation pjax solution. SPX supports advanced pre-fetching capabilities, multiple fragment replacements and snapshot caching engine that prevents subsequent requests from occurring resulting in instantaneous navigations.
- Simple and painless drop-in integration.
- Pre-fetching capabilities using hover, intersection or proximity observers.
- Snapshot caching engine and per-page state control.
- Powerful pub/sub event driven lifecycle triggers.
- Provides a client side DOM hydration approach.
- Supports multiple replace, append and prepend fragment targets.
- Handles inlined and external script evaluation.
- Couples perfectly with stimulus.js.
- Attribute driven programmatic control.
We are using this module live on our webshop.
The landscape of Pjax (Push~State Ajax) solutions has become rather scarce. The current bread winners tend to offer the same thing and we wanted to push the limits. SPX couples together various techniques found to be the most effective in enhancing the performance of SSR rendered web application which fetch pages over the wire.
This module is distributed as ESM and designed to work in the browser environment.
pnpm add spx
Because pnpm is dope and does dope shit.
yarn add spx
Yarn sucks. Choose pnpm and emancipate yourself.
npm i spx
Okay, Boomer...
https://unpkg.com/spx
Be as you are..
To initialize, call spx.connect()
in your bundle preferably before anything else is loaded. By default, the entire <body>
fragment is replaced upon each navigation. You should define a set of targets[]
whose inner contents change on a per-page basis.Consider leveraging the pre-fetching capabilities for the most optimal performance.
The typings provided in this package will describe each option in good detail, below are the defaults. Settings are optional.
import spx from 'spx';
spx.connect({
targets: ['body'],
schema: 'spx',
timeout: 30000,
annotate: false,
cache: true,
limit: 50,
preload: null,
hover: {
trigger: 'attribute',
threshold: 250
},
intersect: {
rootMargin: '',
threshold: 0
},
proximity: {
distance: 85,
throttle: 500,
threshold: 250
},
progress: {
minimum: 0.08,
easing: 'linear',
speed: 200,
trickle: true,
threshold: 500,
trickleSpeed: 200
}
});
Define page fragment targets which are expected to change on a per-page basis. By default, SPX will replace the entire <body>
fragment but it is best to define specific fragments.
Type: string[]
Default: ['body']
By default, attribute identifiers use a -spx-
identifier. You can use a custom attribute identifier or if you wish to opt-out of an identifier you can pass null
so annotations can be expressed using data-
only.
Type: string
Default: spx
Request polling limit is used when a request is already in transit. Request completion is checked every 10ms, by default this is set to 30000
which means requests will wait 30s
before being a new request is triggered.
When true
SPX visits will only trigger on <a data-spx>
href elements annotated containing a data-spx
attribute, otherwise visits will execute naturally. Defaults to false
Type: boolean
Default: false
Determine if page requests should be fetched asynchronously or synchronously. Setting this to false
is not recommended.
Type: boolean
Default: true
Enable or Disable caching. Each page visit request is cached and used in subsequent visits to the same location. Setting this to false
is discourage as all visits will be fetched over the network and data-spx-cache
attribute configs will be ignored.
If
cache
is disabled then prefetches will be dispatched using HTML5<link>
prefetches, else when cache is enabled it uses XHR.
Type: boolean
Default: true
NOT YET AVAILABLE
The persist
option can be used to restore cache into memory after a browser refresh has been triggered. When persisting cache a reference is maintained in session storage.
Type: boolean
Default: false
Cache size limit. This pjax variation limits cache size to 50mb
and once it exceeds that limit, records will be removed starting from the earliest point of known cache entries.
Type: number
Default: 50
Hover pre-fetching. You can disable hover pre-fetching by setting this to false
which will prevent observers from executing and any data-spx-hover
attributes will be ignored. To use the default configurations you can set this to true
or simply omit it.
Type: boolean
or object
Default: { trigger: 'attribute', threshold: 250 }
How hover prefetches should be triggered. By default this option is set to trigger only when <a>
href link elements are attributed with a data-spx-hover
attribute. You can instruct pjax to execute pre-fetching on all <a>
elements by setting this option to href
. If you set the trigger to href
you can annotate links you wish to exclude from prefetch with data-spx-hover="false"
.
Type: string
Accepts: attribute
or href
Default: attribute
Controls the fetch delay threshold. Requests will fire only when the mouse is both within range and the threshold time limit defined here has exceeded.
Type: number
Default: 250
Proximity pre-fetching allow for requests to be dispatched when the cursor is within a proximity range of a href link element. Coupling proximity with mouseover prefetches enable predicative fetching to occur, so a request will trigger before any interaction with a link element happens. To use default behavior, set this to true
and all <a>
annotated with a data-spx-proximity
attribute will be pre-fetched.
Annotate any
<a>
links you wish to exclude from pre-fetching using thedata-spx-proximity="false"
Type: boolean
or object
Default: { distance: 75, throttle: 500, threshold: 250 }
The distance range the mouse should be within before the prefetch is triggered. You can optionally override this by assigning a number value to the proximity attribute. An href element using data-spx-proximity="50"
would inform Pjax to begin fetching when the mouse is within 50px
of the element.
Type: number
Default: 75
Controls the fetch delay threshold. Requests will fire only when the mouse is both within range and the threshold time limit defined here has exceeded.
Type: number
Default: 250
Controls the fetch delay threshold. Requests will fire only when the mouse has exceeded the range and the threshold time limit defined here has been exceeded.
Type: number
Default: 250
Intersection pre-fetching. Intersect pre-fetching leverages the Intersection Observer API to fire requests when elements become visible in viewport. You can disable intersect pre-fetching by setting this to false
, otherwise you can customize the intersect fetching behavior. To use default behavior, set this to true
and all elements annotated with with a data-spx-intersect
or data-spx-intersect="true"
attribute will be pre-fetched. You can annotate elements that contain href links or <a>
elements directly.
Annotate any
<a>
links you wish to exclude from intersection pre-fetching using thedata-spx-intersect="false"
Type: boolean
or object
Default: { rootMargin: '0px 0px 0px 0px', throttle: 0 }
An offset rectangle applied to the root's href bounding box. The option is passed to the Intersection Observer.
Type: string
Default: 0px 0px 0px 0px
Throttle limit passed to the intersection observer instance.
Type: number
Default: 500
Lifecycle events are dispatched to the document upon each navigation. You can access contextual information in the parameters. You can also cancel events with preventDefault()
or by returning boolean false
if you wish to prevent execution from occurring in a certain lifecycle.
The Pjax lifecycle events are dispatched in the following order of execution:
- connected
- prefetch
- visit
- fetch
- store
- route
- render
- load
import spx from 'spx'
spx.on('connected', (state?: IPage, session?: ISession) => void)
spx.on('prefetch', (trigger?: Element, location?: ILocation) => void | false): PrefetchEvent
spx.on('visit', (event?: Event) => void | false): VisitEvent
spx.on('fetch', (state?: IState) => void | false): FetchEvent
spx.on('store', (state?: IState, dom?: Document) => void | false | Document): StoreEvent
spx.on('hydrate', (target?: Element, newTarget?: Element) => void | false): HydrateEvent
spx.on('render', (target?: Element, newTarget?: Element) => void | false): RenderEvent
spx.on('load', (state?: IPage) => void): LoadEvent
The connected event will be triggered after SPX has connected and fired only once. This is the equivalent of the DOMContentLoaded
event. Upon connection, SPX will save the current documents outer HTML to the snapshot cache using document.documentElement.outerHTML
whereas all additional snapshots are saved after an XHR request completes.
Because the initial snapshot is saved using document.documentElement.outerHTML
the captured HTML may cause third party scripts which have augmented the document to serve an invalid dom into the snapshot cache. When a return navigation to this location occurs it may cause the third party script to fail. You can prevent issues of this nature from happening by initializing your modules within the connected
event.
Cancellable: false
Asynchronous: false
The prefetch event will be triggered for every prefetch request. Prefetch requests are fired when hover
, intersect
and proximity
are triggered. This event will be frequently triggered if you are leveraging any of those capabilities. You can determine the type of prefetch which has occurred via the type
parameter.
Cancellable: false
Asynchronous: true
The visit event will be triggered when a mousedown
event has occurred on a SPX enabled href
element. This is the equivalent of a click
and when such an action occurs then navigation intent is assumed and visit begins.
Cancellable: false
Asynchronous: false
The request event will be triggered before an XHR request begins and a page is fetched. This event will be fired for prefetch
, hydrate
and trigger
actions. You can determine the trigger action for the request using the type
property passed in the event.detail
parameter.
Cancellable: false
Asynchronous: true
The cache event will be triggered immediately after a request has finished and before the snapshot and page record is saved to memory. You can determine the trigger action for the request via the type
parameter. This Lifecycle also allows you to augment the snapshot Document
and before it is saved.
Cancellable: false
Asynchronous: false
The render event will be triggered before a page or fragment is rendered (replaced) in the dom. For every target
you've defined this event will fire. You can determine which elements are being replaced via the target
and newTarget
parameters passed. The target
property represents the current element that will be replaced and the newTarget
element represents the new target which it will be replaced with.
Cancellable: false
Asynchronous: false
The hydrate event is identical to the render
event. The parameters represent the current target
and newTarget
elements which will be replaced.
Cancellable: false
Asynchronous: false
The load event is the final lifecycle event to be triggered. Use this event to re-initialize any third party scripts. The load event will only execute after navigation has concluded.
Cancellable: false
Asynchronous: false
The module exposes a low-level routing approach using simple wildcard path matching. Routes executed right before fragments are replaced in the rendering lifecycle. Routes allow you to augment the target documents and state before replacement occur.
import spx from 'spx'
spx.route({
'/:value': (state?: IState, target?: IRoute) => void | Document,
'/path/*': (state?: IState, target?: IRoute) => void | Document,
'/?param': (state?: IState, target?: IRoute) => void | Document,
'/path/:id': {
connect() {},
prefetch() {},
visit() {},
cache() {},
render(){},
load() {},
exit() {}
}
});
In addition to Lifecycle events, you also have a list of methods available. Methods will allow you some basic programmatic control of the SPX session occurring, provides access to the cache store and various other operational utilities.
import spx from 'spx'
spx.supported: boolean
spx.connect(options?: {}): void
spx.session(store?: string, merge?:{}): ISession
spx.hydrate(url?: string, targets: string[]): Promise<IPage>
spx.fetch(url: string): Promise<Document>
spx.prefetch(string | Element): Promise<IPage>
spx.visit(url: string, options?:{}): Promise<IPage>
spx.state(url?: string, merge?:{}): Page{}
spx.capture(targets: string[]): Promise<Element[]>
spx.clear(url?: string): void
spx.reload(): IPage
spx.disconnect(): void
The connect
method is a required call and will initialize a SPX session. You can optionally provide options which inform SPX on how it should behave. See options for list of settings.
Returns: void
Dispatched Events: connected
The session
method will return the current store instance. This includes all state, snapshots, options and settings of the current session which exists in memory. If you intend of augmenting the session, please note that the store records are created without prototype.
Returns: Promise<IPage>
The hydrate
method executed a programmatic hydration. The method expects a url
and string list of element selectors.
Returns: Promise<IPage>
Events: cache > hydrate > load
Triggers a programmatic fetch. The XHR request response is not cached and no state reference are touched.
Returns: Document
The prefetch
method executed a programmatic Prefetch. The method expects a url
or <a href="*"></a>
node as an argument. This method behaves the same way as hover, intersect of proximity prefetches.
Returns: Promise<IPage>
Events: request > cache
The visit
method executed a programmatic trigger visit. The method expects a url
as an argument and optionally accepts an page state options model. This method behaves the same way as trigger.
Returns: Promise<IPage>
Events: replace > request > cache > render > load
The state
method returns the records pertaining to the provided url
or if not defined returns the current location. Optionally pass a state
object reference to merge and augment the current references.
Returns: Promise<IPage>
The capture
method performs a snapshot modification to the current document. Use this to align a snapshot cache record between navigations. This is helpful in situations where the dom is augmented and you want to preserve
Returns: Promise<IPage>
This SPX variation is leveraging modern browser capabilities. What makes this SPX variant faster than others is how the pages are fetched and the caching approach it employs.
Pages are fetches using XHR opposed to the Fetch API as we are dealing with HTML requests there is no benefit of using Fetch over XHR.
TODO
- An XHR request can begin on mousedown, mouseover, element intersection or via cursor proximity.
- The response DOM string of fetched pages is stored in memory, so page requests are only ever executed once.
- Stored pages (snapshots) are re-used when returning visits to cached (stored) locations occur.
- The DOM Parser API is used in the rendering cycle, only specific elements (targets) are replaced.
- The state model of the History API is used maintain page specific configuration references.
This module is written in TypeScript. Production bundles exports to ES2015. This project has been open sourced from within a predominantly closed source mono/multi repo. We will update it according to what we need. Feel free to suggest features or report bugs and PR's are welcome!
The project is functional in its architecture, there are no classes, just functions. The observers directory contains the various fetch and pre-fetch logics. Objects avoid the prototype and Object.create(null)
is the preferred approach.
The project is fairly easy to understand, there are no complexities and over-engineering. The Pjax method is simple, you fetch pages over the wire and replace elements in the rendering cycle. This module follows this pattern but includes additional extras to help improve the rendering times.
This module combines concepts originally introduced by other awesome Open Source projects:
Special Thanks/Спасибі to Alexey for the SPX registry name.
Licensed under MIT
We ♡ open source!