Blazing fast, lightweight (9kb gzipped), feature full drop-in next generation pjax for SSR web applications. This pjax variation supports multiple fragment replacements, advanced pre-fetching capabilities executing via mouse/pointer/touch or intersection events and provides a snapshot caching engine which prevents subsequent requests from occurring resulting in instantaneous page navigations.
The landscape of pjax based solution has become rather scarce. The current bread winners either offer the same thing or for our use cases were vastly over engineered and rather shitty tbh. This pjax variation couples together various techniques I found to be the most effective in enhancing the performance of SSR rendered web application that fetch pages over the wire. The pjax approach is tried and tested in the web nexus and while many folks tend to dismiss this tactic for improving TTFB load times of web applications there is actually very little difference in terms of rendering speed when this technique is appropriated correctly and in some cases it can outperform the rendering performance of a virtual dom library.
Below is a real world example you can use to better understand how this module works so you can apply it into your web application. We are working on providing a live demonstration for more advanced use cases, but the below example should give you a good understanding and help you in understanding how to leverage the module.
Lifecycle Events
Lifecycle events are dispatched to the document upon each navigation. You can access context information from within event.detail or cancel events with preventDefault() and prevent execution.
document.addEventListener("pjax:prefetch");
document.addEventListener("pjax:trigger");
document.addEventListener("pjax:request");
document.addEventListener("pjax:cache");
document.addEventListener("pjax:render");
document.addEventListener("pjax:load");
document.addEventListener("pjax:module");
Methods
In addition to Lifecycle events, a list of methods are available. Methods will allow you some basic programmatic control of a Pjax session.
Pjax.supported: boolean
Pjax.connect(options?): void
Pjax.visit(url?, options?): Promise<Page{}>
Pjax.cache(url?): Page{}
Pjax.clear(url?): void
Pjax.uuid(size = 16): string
Pjax.reload(): Page{}
Pjax.disconnect(): void
Attributes
Link elements can be annotated with data-pjax attributes. You can control how pages are rendered by passing the below attributes on <a> nodes.
data-pjax-eval
Used on resources contained within <head> fragment like styles and scripts. Use this attribute if you want pjax the evaluate scripts and/or stylesheets. This option accepts a false value so you can define which scripts to execute on each navigation. By default, pjax will run and evaluate all <script> tags it detects for every page visit but will not re-evaluate <script src="*"></script> tags.
If a script tag is detected on pjax navigation and is using data-pjax-eval="false" it will execute only once upon the first visit but never again after that.
Example
<script>
console.log('I will run on every navigation');
</script>
<script data-pjax-eval="false">
console.log('I will run on initialization only!');
</script>
data-pjax-disable
Place on href elements you don't want pjax navigation to be executed. When a link element is annotated with data-pjax-disable a normal page navigation will be executed and cache will be cleared.
Example
Clicking this link will clear cache and a normal page navigation will be executed.
<a href="*" data-pjax-disable></a>
data-pjax-track
Place on elements to track on a per-page basis that might otherwise not be contained within target elements.
Example
Lets assume you are navigating from Page 1 to Page 2 and #main is your defined target. When you navigate from Page 1 only the #main target will be replaced and any other dom elements will be skipped which are not contained within #main. Elements located outside of target/s that do no exist on previous or future pages will be added.
Page 1
<nav>
<a href="/page-1" class="active">Page 1</a>
<a href="/page-2">Page 2</a>
</nav>
<div id="#main">
<div class="block">I will be replaced, I am active on every page.</div>
</div>
Page 2
<nav>
<a href="/page-1">Page 1</a>
<a href="/page-2" class="active">Page 2</a>
</nav>
<div id="#main">
<div class="block">I will be replaced, I am active on every page.</div>
</div>
<div data-pjax-track>
I am outside of target and will be tracked if Pjax was initialized on Page 1
</div>
<div>I will not be tracked unless Pjax was initialized on Page 2</div>
If pjax was initialized on Page 2 then Pjax would have knowledge of its existence before navigation. In such a situation, pjax will mark the tracked element internally.
data-pjax-hydrate
Executes a replacement of the defined elements. Hydrate is different from replace, append and prepend methods in the sense that the those are combined with your defined targets. When calling Hydrate, it will run precedence over targets and for the visit it will replace only the element/s provided.
(['target'])
(['target' , 'target'])
Example
<a
href="*"
data-pjax-hydrate="(['.price', '.cart-count'])">
Link
</a>
<div>
The next navigation will replace all elements with class "price"
and the elements with class "cart-count" - If you have defined
the element "#main" as a "target" in your connection, a replacement
will not be made on that element, instead the elements defined in
"data-pjax-hydrate" will become the target/s.
</div>
<span class="cart-count">1</span>
<span class="price">€ 450</span>
<div id="main">
<img src="*">
<ul>
<li>Great Product</li>
<li class="price">€ 100</li>
<li>Awesome Product</li>
<li class="price">€ 200</li>
<li>Cool Product</li>
<li class="price">€ 300</li>
</ul>
</div>
data-pjax-replace
Executes a replacement of defined targets, where each target defined in the array is replaced.
(['target'])
(['target' , 'target'])
Example
<a
href="*"
data-pjax-replace="(['#target1', '#target2'])">
Link
</a>
<div id="target1">
I will be replaced on next navigation
</div>
<div id="target2">
I will be replaced on next navigation
</div>
data-pjax-prepend
Executes a prepend visit, where [0] will prepend itself to [1] defined in that value. Multiple prepend actions can be defined. Each prepend action is recorded are marked.
(['target' , 'target'])
(['target' , 'target'], ['target' , 'target'])
Example
PAGE 1
<a
href="*"
data-pjax-prepend="(['#target-1', '#target-2'])">
Page 2
</a>
<div id="target-1">
I will prepend to target-2 on next navigation
</div>
<div id="target-2">
<p>target-1 will prepended to me on next navigation</p>
</div>
PAGE 2
<a
href="*"
data-pjax-prepend="(['#target-1', '#target-2'])">
Page 2
</a>
<div id="target-2">
<div data-pjax-action="xxxxxxx">
I am target-1 and have been prepended to target-2
</div>
<p>target-1 is now prepended to me</p>
</div>
data-pjax-prefetch
Prefetch option to execute. Accepts either intersect or hover value. When intersect is provided a request will be dispatched and cached upon visibility via Intersection Observer, whereas hover will dispatch a request upon a pointerover (mouseover) event.
On mobile devices the hover value will execute on a touchstart event
Example
<a data-pjax-prefetch="hover" href="*"></a>
<a data-pjax-prefetch="intersect" href="*"></a>
data-pjax-threshold
Set the threshold delay timeout for hover prefetches. By default, this will be set to 100 or whatever preset configuration was defined in Pjax.connect() but you can override those settings by annotating the link with this attribute.
Example
<a data-pjax-prefetch="hover" href="*"></a>
<a data-pjax-prefetch="hover" data-pjax-threshold="500" href="*"></a>
<a data-pjax-prefetch="intersect" href="*"></a>
data-pjax-position
Scroll position of the next navigation. Space separated expression with colon separated prop and value.
Example
<a data-pjax-position="y:1000 x:0" href="*"></a>
data-pjax-cache
Controls the caching engine for the link navigation. Accepts false, reset or clear value. Passing in false will execute a pjax visit that will not be saved to cache and if the link exists in cache it will be removed. When passing reset the cache record will be removed, a new pjax visit will be executed and its result saved to cache. The clear option will clear the entire cache.
Example
<a data-pjax-cache="false" href="*"></a>
data-pjax-history
Controls the history pushstate for the navigation. Accepts false, replace or push value. Passing in falsewill prevent this navigation from being added to history. Passing in replace or push will execute its respective value to pushstate to history.
Example
<a data-pjax-history="false" href="*"></a>
data-pjax-progress
Controls the progress bar delay. By default, progress will use the threshold defined in configuration presets defined upon connection, else it will use the value defined on link attributes. Passing in a value of false will disable the progress from showing.
Example
<a data-pjax-progress="500" href="*"></a>
State
Each page has an object state value. Page state is immutable and created for every unique url /path or /pathname?query=param value encountered throughout a pjax session. The state value of each page is added to its pertaining History stack record.
Navigation sessions begin once a Pjax connection has been established and ends when a browser refresh is executed or url origin changes.
Read
You can access a readonly copy of page state via the event.details.state property within dispatched lifecycle events or via the Pjax.cache() method. The caching engine used by this Pjax variation acts as mediator when a session begins, so when you access page state via the Pjax.cache() method you are given a bridge to the Map object of all active sessions cache data.
Write
State modifications are carried out via link attributes or when executing a programmatic visit using the Pjax.visit() method. The visit method provides an options parameter for adjustments to be merged. Though this method will only allow you to modify the next navigation you should avoid modifying state outside of the available methods, instead treat it as readonly.
interface IPage {
readonly targets?: string[];
url?: string;
snapshot?: string;
title?: string;
history?: boolean;
hyrdate?: null | string[];
replace?: null | string[];
append?: null | Array<[from: string, to: string]>;
prepend?: null | Array<[from: string, to: string]>;
cache?: boolean | 'reset' | 'clear';
threshold?: number;
proximity?: number;
progress?: boolean | number;
position?: {
y: number;
x: number;
};
location?: {
origin?: string;
hostname?: string;
pathname?: string;
search?: string;
hash?: string;
lastpath?: string;
};
}
Contributing
This module is written in TypeScript. Production bundles export in ES2015 format. Legacy support is provided as an ES5 UMD bundle. This project leverages JSDocs and Type Definition files for its type checking, so all features you enjoy with TypeScript are available.
This module is consumed by us for a couple of our projects and has been open sourced but exists as part of a mono/multi repo. We will update it according to what we need. Feel free to suggest features or report bugs, PR's are of course welcome!
Acknowledgements
This module combines concepts originally introduced by other awesome Open Source projects and owes its creation and overall approach to those originally creators: