Loader
Role
Loader.js owns the first-load experience and the route asset preparation layer.
It is intentionally separate from page transitions so the project can reason about:
- initial loading
- critical asset preparation
- deferred asset loading
- route change animation
as related but distinct concerns.
Main Files
src/themes/reactwp/js/inc/Loader.jssrc/themes/reactwp/js/inc/Cache.jssrc/themes/reactwp/js/inc/config/configureLoader.js
Responsibilities
The loader runtime is responsible for:
- animating
#loaderon first load - preloading the active route template
- preloading critical fonts
- preloading critical media
- rendering critical media into its declared target before the route reveal finishes
- starting non-critical media downloads after the route is ready
- rendering deferred media after the critical reveal is complete, group by group as each group becomes ready
It is also the runtime that merges the route's declared media groups with the global all group from the bootstrap payload.
If a route declares:
home, shared
the loader will look at:
allhomeshared
for critical fonts, critical medias, and non-critical medias.
During the deferred phase, ReactWP still downloads non-critical groups in parallel, but it no longer waits for every group to finish before rendering. Once the critical reveal is complete, each non-critical group can render as soon as that group's own download is ready.
Runtime State
ReactWP also exposes an observable loader state on window.loader.
This state is not the Loader module itself.
It is a plain object that contains:
- simple state values
- Promise-based loading hooks
Plain Values
These values are not Promises:
window.loader.routewindow.loader.isLoaded
window.loader.route is the route currently tracked by the loader.
window.loader.isLoaded becomes true when the loader considers the current route loaded enough to finish its critical phase.
Promise Values
These values are Promises and can be used with .then():
window.loader.initwindow.loader.criticalDisplaywindow.loader.noCriticalDisplaywindow.loader.templatewindow.loader.criticalFontswindow.loader.criticalMediaswindow.loader.noCriticalMediaswindow.loader.criticalDownloadwindow.loader.noCriticalDownload
Group-Level Promise Maps
These values are plain objects whose values are Promises keyed by media group name:
window.loader.noCriticalDownloadGroupswindow.loader.noCriticalDisplayGroups
Example:
window.loader.noCriticalDownloadGroups.home?.then(() => {
console.log('The home deferred group finished downloading.');
});
window.loader.noCriticalDisplayGroups.home?.then(() => {
console.log('The home deferred group finished rendering.');
});
Meaning Of Each Promise
window.loader.init: first-load lifecycle only. Resolves when the initial critical route is ready and the initial loader animation is resolved.window.loader.criticalDisplay: resolves only after the critical route batch is downloaded, the route is ready, and critical medias have been rendered into their targets.window.loader.noCriticalDisplay: resolves when the full non-critical display phase is finished across all groups.window.loader.template: resolves when the current route template chunk is ready.window.loader.criticalFonts: resolves when the critical font loading phase is finished.window.loader.criticalMedias: resolves when the critical media loading phase is finished.window.loader.noCriticalMedias: resolves when the deferred media loading phase is finished.window.loader.criticalDownload: resolves when the full critical batch is finished. This includes template, critical fonts, and critical medias.window.loader.noCriticalDownload: resolves when the non-critical batch is finished across all groups.window.loader.noCriticalDownloadGroups.<group>: resolves when one specific non-critical group finishes downloading.window.loader.noCriticalDisplayGroups.<group>: resolves when one specific non-critical group finishes rendering.
These Promises are meant for observation and orchestration. They do not expose internal request objects directly.
Examples:
console.log(window.loader.route);
console.log(window.loader.isLoaded);
window.loader.init.then(() => {
console.log('Initial loader animation is finished.');
});
window.loader.template.then(() => {
console.log('Route template is ready.');
});
window.loader.criticalFonts.then(() => {
console.log('Critical fonts are ready.');
});
window.loader.criticalMedias.then(() => {
console.log('Critical medias are ready.');
});
window.loader.criticalDownload.then(() => {
console.log('Critical template, fonts, and media are ready.');
});
window.loader.noCriticalMedias.then(() => {
console.log('Deferred media finished downloading.');
});
window.loader.noCriticalDownload.then(() => {
console.log('Non-critical downloads are finished.');
});
window.loader.noCriticalDownloadGroups.home?.then(() => {
console.log('The home deferred group finished downloading.');
});
window.loader.noCriticalDisplayGroups.home?.then(() => {
console.log('The home deferred group finished rendering.');
});
window.loader.criticalDisplay.then(() => {
console.log('The current route can be revealed.');
});
window.loader.noCriticalDisplay.then(() => {
console.log('The non-critical display phase is finished.');
});
Use these promises for debugging, instrumentation, or project-specific orchestration.
Request Reuse
The loader keeps request maps for:
- media fetches
- font loads
- critical route preparation
- deferred route preparation
That means repeated visits in the same session can reuse work that has already been prepared.
Cache.js also keeps browser-side media cache entries and in-memory blob URLs so critical media can be downloaded once and then reused by the loader runtime.
This is separate from the route payload cache in RouteService.js.
Failure Behavior
The loader uses a tolerant strategy for asset preparation.
Critical and deferred batches resolve after their loading attempts settle. In other words, the runtime does not require every asset request to succeed before moving forward.
This keeps route changes from hanging forever because of one failed media request.
If a media entry has no matching target, ReactWP still treats the download as complete, but skips the DOM replacement step for that entry.
Media Entry Shape
Critical and deferred media groups can register entries such as:
[
'type' => 'image',
'src' => '/wp-content/uploads/2026/04/hero-desktop.jpg',
'target' => '#hero .media-slot',
'alt' => 'Hero image',
'sources' => [
[
'src' => '/wp-content/uploads/2026/04/hero-mobile.jpg',
'media' => '(max-width: 767px)'
]
]
]
Supported concepts include:
image,video, andaudiotargetas a selector, DOM node, or array of selectors/nodessourcesfor image<picture>sources or media<source>children- normal DOM props that should land on the rendered element
That means a critical media entry can do more than just “download this URL”. It can also replace a placeholder node with the final media element at the right moment in the route lifecycle.
Reduced Motion
The loader runtime also supports reduced motion.
If the user prefers reduced motion, ReactWP can skip the full animated sequence and use an immediate fallback instead.
Loader.setAnimation() accepts two functions:
- the main animation factory
- an optional immediate factory used for reduced motion
Example:
import { Loader } from './Loader';
export const configureLoader = () => {
Loader.setAnimation(
({ gsap, done }) => {
return gsap.to('#loader', {
duration: 0.35,
opacity: 0,
pointerEvents: 'none',
onComplete: done
});
},
({ gsap }) => {
gsap.set('#loader', {
opacity: 0,
pointerEvents: 'none'
});
}
);
};
Use this when you want:
- a full animated loader for normal motion
- an immediate non-animated fallback for reduced motion
Customization
Use config/configureLoader.js to replace the default first-load animation.
Example:
import { Loader } from './Loader';
export const configureLoader = () => {
Loader.setAnimation(({ gsap, ScrollTrigger, done }) => {
let tl = gsap.timeline({
onComplete: () => {
tl.kill();
tl = null;
done();
}
});
tl
.to({}, {
duration: 0.1
})
.add(() => {
if(!window.loader?.isLoaded){
tl.restart();
return;
}
})
.to('#loader', {
duration: 0.4,
opacity: 0,
pointerEvents: 'none',
onStart: () => {
ScrollTrigger?.refresh();
}
});
return tl;
});
};
In that pattern:
- the loader animation starts immediately
- the timeline can loop while
window.loader.isLoadedis stillfalse - ReactWP flips
window.loader.isLoadedtotrueonly after the critical route is ready to reveal - then the timeline can play its exit sequence and call
done()
The runtime restores the default loader state before configureLoader.js runs, so projects can focus on the animation override itself.