The theme boots in two phases, owned by two body-mounted Globals:
- Crucial (
critical.js) runs eagerly, on the critical path. - Deferred (
public.js) runs on the first user interaction.
Both are just Instantiables whose init() builds a
Processor and drains it. What differs is when
they run and what they're allowed to do — and that difference is the theme's entire
performance story.
Presence-gated lazy loading is the organizing principle
Neither phase ships a fixed bundle. Every entry in both lists is a
{ name, selector, loader } descriptor that loads only if its selector matches an
element on the page. Across Crucial, Deferred, and every HasX boot, the same gate
applies — so, layered over Vite's per-component code-splitting, a page downloads
exactly the components it contains and nothing else.
Crucial — declare, mostly don't instantiate
Crucial's job on the critical path is to get above-the-fold CSS and JS ready without
paying for their layout work yet. Most of its entries are style-only
(name: null): the chunk downloads and its CSS applies, but no instance is created.
processor
.convert({
name: null, // style-only
selector: '.masthead',
loader: () => import('../../modules/Masthead/Masthead.js'),
})
.convert({
name: 'HeaderBar', // genuinely interactive → fully loaded
selector: '.header-bar',
loader: () => import('../../layouts/HeaderBar/HeaderBar.js'),
})
Only truly interactive chrome — HeaderBar, Accessibility — is fully named and
instantiated here. Everything above the fold that would trigger reflow is held as
style-only.
Deferred — do the real work, after first paint
Deferred loads on the first interaction and re-declares those same modules fully
named, instantiating them and running all their layout manipulation:
processor
.convert({
name: 'Masthead', // same module, now instantiated
selector: '.masthead',
loader: () => import('../../modules/Masthead/Masthead.js'),
})
// …every other module on the page
By now the chunk is already fetched and parsed (Crucial did that), so the deferred instantiation is cheap and the expensive reflow lands after the metrics PageSpeed measures.
Why it works this way. Many above-the-fold modules trigger reflow when they initialize, which tanks the PageSpeed score if it happens on the critical path. So loading is split: Crucial withholds instantiation (style-only), and Deferred does it after first paint. The JS is already parsed, so deferred instantiation is cheap and the reflow-causing work lands after the measured metrics. (ADR 0003.)
The intentional duplication
A module that must paint above the fold therefore appears in both lists — style-only in Crucial, fully named in Deferred (Masthead, PageHeader, Message, the OneFold / SinglePost templates). This is not redundancy; it is the two halves of one decision: load the styles now, withhold the instantiation until later.
Some Modules (Masthead, PageHeader, Message) sit in Crucial because they're almost always the first module on the page — a loading heuristic about above-the-fold placement, not a category. They are ordinary Modules in the editor; nothing distinguishes them there.
What "first interaction" means in production
Deferred is public.js. In production, WP Rocket delays JS execution until the
first user interaction (scroll, tap, key), which is what actually triggers the deferred
phase. A 10-second timer is only a fallback for the no-interaction / no-JS-delay
case — it is not a perf bug, and it is not the normal path.
A consequence of all this: a module's full interactive behaviour is not available
until the first interaction. Anything an external script needs to touch on a booted
instance must wait for that — which is why the reliable hook is the
initialized event, not a synchronous registry read at page load. See
The Globals registry.
Related
- The AssetLoader pipeline — the Processor both phases drive, and
name: null. - The Globals registry — why external hooks must wait for the deferred phase.
- The lifecycle — the instantiation Crucial withholds and Deferred performs.