Skip to main content

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.js
  • src/themes/reactwp/js/inc/Cache.js
  • src/themes/reactwp/js/inc/config/configureLoader.js

Responsibilities

The loader runtime is responsible for:

  • animating #loader on 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.

On a valid static/server direct request, ReactWP enqueues the extracted template CSS and skips the blocking loader by default so pre-rendered HTML can paint before hydration. The loader still prepares the template and route assets in the browser. Return false from rwp_prerender_skip_loader when an existing project must finish imperative critical-media insertion before its first reveal.

If a route declares:

home, shared

the loader will look at:

  • all
  • home
  • shared

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.route
  • window.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.init
  • window.loader.criticalDisplay
  • window.loader.noCriticalDisplay
  • window.loader.template
  • window.loader.criticalFonts
  • window.loader.criticalMedias
  • window.loader.noCriticalMedias
  • window.loader.criticalDownload
  • window.loader.noCriticalDownload

Group-Level Promise Maps

These values are plain objects whose values are Promises keyed by media group name:

  • window.loader.noCriticalDownloadGroups
  • window.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.

Loader Module API

The exported module object provides these callable methods:

MethodContract
configure()restore defaults, create a new stable initial-load Promise, reset window.loader, and synchronize the empty route state
reset()restore the default animation and clear module lifecycle references; it does not delete browser caches or the module-level request maps
state(reset = false)return window.loader (or the server fallback state); true recreates and synchronizes it
syncState(route = currentRoute)point the observable Promise fields at the cached critical/deferred request objects for the route
setAnimation(factory, immediateFactory = null)set the main loader animation and optional reduced-motion implementation
setRoute(route)set the tracked route, reset deferred display reuse for the previous route, and create a new route-ready Promise when identity changes
resetRouteReady(path = null)replace the route-ready Promise for one route key
markRouteReady(path = null)resolve route readiness; returns false when a supplied key does not match the pending key
waitForRouteReady(timeout = 10000)wait for readiness or the bounded timeout, then wait for a paint
waitForPaint(frames = 2)resolve after the requested number of animation frames
route()return the tracked route, then the initial runtime route, then null
setLabel(label)write non-empty text to #loader .loader-label when it exists
play() / init()start the configured animation lifecycle and return its Promise
whenInitialLoadDone()observe the existing initial lifecycle without starting or replaying the animation
download(routeOrPath = route())resolve an optional path, prepare critical and deferred work, and return the critical-download Promise
prepareRoute(routeOrPath)resolve an optional path, start both phases, wait for critical download, and return the route
preloadDeferred(route)start/reuse deferred downloads without waiting for them
prepareInitialLoad(route = runtime.route)bind the initial route and start its critical and deferred requests
finishInitialLoad(route = runtime.route)run the one stable initial critical-display plus loader-animation lifecycle
display(route = route())wait for critical download and route readiness, render critical media, then mark the route loaded
noCriticalDisplay(route = route())render deferred groups after critical display, preserving one display Promise per route/group

configure(), prepareInitialLoad(), and finishInitialLoad() are normally orchestrated by useRouteTransition(). Calling them a second time from a template can replace observable state or duplicate lifecycle work; project templates generally need only observation methods or prepareRoute() for explicit prefetching.

Examples:

import { Loader } from '../inc/Loader';

console.log(window.loader.route);
console.log(window.loader.isLoaded);

window.loader.init.then(() => {
console.log('Initial loader animation is finished.');
});

// Equivalent module API. This observes the existing lifecycle and never
// starts or replays the loader animation.
Loader.whenInitialLoadDone().then(() => {
console.log('Initial loader lifecycle 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.

Those Cache Storage names include runtime.system.cacheVersion. A new server generation creates new JSON/media cache names and removes older ReactWP generations on the next full page load.

This is separate from the route payload cache in RouteService.js.

See Media Pipeline for authoring entries and Cache and Invalidation for release behavior.

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, and audio
  • target as a selector, DOM node, or array of selectors/nodes
  • sources for image <picture> sources or media <source> children
  • allowlisted media DOM props that should land on the rendered element

Selector, NodeList, and HTMLCollection resolution is capped at 100 targets per value. ReactWP refuses to replace structural nodes including html, head, body, script/style/metadata nodes, embeds/forms, and the shell IDs app, app-header, viewport, pageWrapper, and pageContent.

Media/source URLs must resolve to http:, https:, or a generated blob: URL. poster is validated separately. Event attributes are rejected; style and dataset objects are capped at 100 properties; individual style values are capped at 2048 characters; ordinary forwarded string/number values are capped at 4096 characters.

The loader discards event-handler keys, unsupported properties, unsafe media URLs, and unsafe or oversized style values instead of applying arbitrary entry data to the DOM.

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

When a factory accesses done, that callback becomes the authoritative completion signal. ReactWP will not resolve the loader merely because the returned GSAP timeline completes. This allows asynchronous work or a delayed callback to keep window.loader.init and Loader.whenInitialLoadDone() pending.

When a factory does not access done, ReactWP automatically observes the returned GSAP animation or Promise. Calling Loader.init() or Loader.play() starts an animation; use Loader.whenInitialLoadDone() when a component only needs to observe the initial loader.

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.isLoaded is still false
  • ReactWP flips window.loader.isLoaded to true only 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.