Every page should earn its JavaScript and CSS.
Engineering a content-aware WordPress asset pipeline: from ACF composition and stylesheet ordering to genuinely optional JavaScript.
A page is a set of capabilities, not a request to download everything the website can do. That distinction changes how we build WordPress themes.
A directory may need a location picker, a map, filters and a results controller. Its privacy policy probably needs none of them. Yet a site-wide stylesheet and a single JavaScript bundle can make both pages pay for the same machinery. Compressing that bundle leaves the architectural decision intact.
The useful question is not “How can we load this file faster?” It is “Why is this request responsible for this file at all?” Answering it requires a relationship between content, rendering and dependencies that the application can inspect.
Start with a component contract
Give each component a name and a declared resource contract. The contract describes its stylesheet, initial behaviour and optional capabilities. It should not depend on a list of page IDs that becomes inaccurate as soon as someone edits the site.
For an ACF-driven site, the page’s row and element definitions provide the starting inventory. For a block-based site, the block tree provides another. Neither is automatically the final answer: templates can introduce components, shared content can reference other content, and dynamic rendering can depend on the current visitor.
A small renderer can therefore work in two phases. First, resolve the components that will participate in the response. Second, render those components using the same resolved structure. This avoids maintaining one interpretation of the page for assets and another for HTML.
Collect before you print
Styles needed for the initial layout should be known before the document head is printed. Discovering a component for the first time halfway through the body is too late to guarantee that its CSS arrives without a flash or a layout shift. Moving all styles into the footer is not a solution to that ordering problem.
The following example deliberately separates an application-specific discovery function from WordPress’s asset API. enjin_components_for_request() is a contract you must implement for your renderer; it is not a WordPress function. Asset paths are illustrative, and the version should come from your release manifest.
add_action('wp_enqueue_scripts', function () {
$manifest = [
'directory' => [
'css' => '/assets/directory.css',
'js' => '/assets/directory.js',
'deps' => [],
],
'enquiry' => [
'css' => '/assets/enquiry.css',
'js' => null,
'deps' => [],
],
];
foreach (array_unique(enjin_components_for_request()) as $name) {
if (!isset($manifest[$name])) {
continue;
}
$asset = $manifest[$name];
$handle = 'enjin-' . $name;
$base = get_stylesheet_directory_uri();
wp_enqueue_style($handle, $base . $asset['css'], [], '2026.09');
if ($asset['js']) {
wp_enqueue_script($handle, $base . $asset['js'], $asset['deps'],
'2026.09', ['strategy' => 'defer', 'in_footer' => true]);
}
}
});The names in the manifest are an allowlist. Content selects a capability, never an arbitrary filesystem path or script URL. Register dependent handles before enqueueing their consumers. WordPress considers those dependency relationships when deciding which deferred strategy is eligible; an intended strategy is not a promise that every resulting tag will carry that attribute. See the script-loading reference.
CSS needs an ordering contract, too
Conditional CSS is not simply the JavaScript manifest with different file extensions. A component’s appearance can depend on the order of declarations elsewhere in the document. Splitting a stylesheet can expose a hidden assumption that one selector always wins because its file happened to load last.
Keep a small shared foundation for tokens, typography and layout primitives. Let components own their additional rules. Define a stable cascade order, then verify that composing the same components in different page arrangements does not change their appearance accidentally.
Cascade layers can make that order explicit, but introducing them around a legacy theme needs care: normal unlayered author styles outrank normal layered styles. A plugin’s unlayered rules can therefore win even when the new component selector appears more specific. Audit the whole cascade before treating layers as isolation. The cascade-layer reference describes the precedence rules, including the reversed order for important declarations.
Keep initial-layout CSS in the head and optional UI hidden until its styles are ready. For a dynamically imported map, model stylesheet readiness as part of the module’s mounting contract. Test a slow stylesheet response as well as a slow script response. Deferring CSS that the visitor can already see can introduce a flash of unstyled content or a layout shift, even when the final screenshot looks perfect.
Conditional loading and code splitting are different decisions
Conditional loading chooses whether a page needs an asset. Code splitting decides where the boundaries inside the JavaScript application belong. You can do the first without webpack, Vite or any build tool. You can also produce many chunks and still load all of them on every page.
Consider a directory whose results are useful without a map. Serve the list and filter form as working HTML. Load the lightweight directory controller initially. Fetch the map module only when the visitor requests the map, or slightly earlier when a measured interaction pattern justifies it. The browser’s dynamic import provides this boundary without prescribing a particular bundler.
const button = document.querySelector('[data-open-map]');
const panel = document.querySelector('[data-map-panel]');
let mapModule;
if (button && panel) {
button.addEventListener('click', async () => {
button.disabled = true;
button.setAttribute('aria-busy', 'true');
try {
mapModule ??= import('./directory-map.js');
const { mountMap } = await mapModule;
// mountMap must tolerate repeated calls for the same panel.
await mountMap(panel);
panel.hidden = false;
button.setAttribute('aria-expanded', 'true');
} catch {
mapModule = undefined; // Permit another attempt after a network failure.
const status = document.querySelector('[data-map-status]');
if (status) status.textContent = 'The map could not load. The list is still available.';
} finally {
button.disabled = false;
button.removeAttribute('aria-busy');
}
});
}The associated markup needs a live status region, aria-controls, an initial aria-expanded="false" and a hidden map panel. The module must own its CSS and external-library readiness as well as its JavaScript. Splitting only the script while globally loading a large map stylesheet solves half the problem.
Do not move the entire cost onto the first click
Deferring expensive work can make a loading score look excellent while making the first interaction miserable. A map click that triggers module download, library parsing, synchronous data transformation and hundreds of DOM updates is still expensive. It merely happens later.
Measure that interaction on a representative device. Separate network delay from main-thread execution. Reduce the initial dataset, batch DOM changes and move suitable computation into a worker only when the transfer and coordination costs justify it. Keep an immediately visible pending state; never use a silent wait as the interface.
Prefetching is a trade-off, too. Aggressive prefetching can download the very modules you meant to avoid for people who never use them. Treat it as a hypothesis to test against actual navigation behaviour, not a compulsory finishing step.
Dequeue with knowledge of the dependency tree
A plugin’s globally enqueued script is a reasonable audit target. It is not automatically safe to remove. Another handle may depend on it; inline configuration may be attached to it; an embedded form may introduce it on a page that previously had no forms.
Inspect registrations, dependency edges and the final rendered tags. Prefer the plugin’s documented conditional-loading mechanism when it offers one. If you replace a handle, preserve the consumers’ expectations and test the feature’s less obvious states: validation errors, AJAX responses, translations and authenticated views. Do not deregister a shared library merely because the visible homepage appears to work without it.
Make absence part of the test suite
Use a small matrix of pages and capabilities. The editorial page should have no directory assets. The directory should function without opening its map. The enquiry page should retain validation. A dynamic component added by an editor should cause the correct assets to appear without a developer updating a URL list.
Record transfer size, JavaScript parse and execution time, layout stability and the delay of the first meaningful interaction. Compare the same browser, device profile, cache state and content. A lower request count alone proves very little: several small, reusable assets can be better than one large bundle.
The resulting architecture is useful beyond performance. A component’s dependencies become visible, reviewable and testable. The site starts delivering what each page actually needs, and an editor can change the composition without silently breaking that agreement.
Code examples illustrate the architecture and identify application-specific interfaces. Adapt them to your environment and test the failure paths before deployment. Sources are linked alongside the relevant discussion.