This document explains how to implement and use the Dynamic Asset Loader system in the Extend WP plugin. The Dynamic Asset Loader improves page load performance by loading scripts and styles only when their corresponding DOM elements are present on the page.
💡 What is Dynamic Asset Loading?
Dynamic Asset Loading is a performance optimization technique that loads JavaScript and CSS files on-demand based on DOM element presence. This eliminates unnecessary HTTP requests, reduces initial page weight, and improves Core Web Vitals metrics (FCP, LCP, TTI).
1. How It Works #
The system operates in two phases:
PHP Phase (Server-Side) #
- Developers register assets using the
ewp_register_dynamic_assetsfilter - Assets are validated and sanitized
- Configuration is passed to JavaScript via
wp_localize_script - Resource hints and preload links are added to
<head> - Critical CSS is inlined in
<head>(priority 3)
JavaScript Phase (Client-Side) #
- The loader initializes on
DOMContentLoaded - Checks DOM once for all registered selectors
- Loads assets immediately when selectors are found
- Developers manually trigger
checkAssets()after AJAX/dynamic content
2. Basic Registration #
Minimal Script Example #
add_filter('ewp_register_dynamic_assets', function($assets) {
$assets[] = array(
'handle' => 'my-widget',
'selector' => '.my-widget',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/widget.js',
'context' => 'both' // 'frontend', 'admin', or 'both' (default)
);
return $assets;
});
Minimal Style Example #
add_filter('ewp_register_dynamic_assets', function($assets) {
$assets[] = array(
'handle' => 'my-widget-styles',
'selector' => '.my-widget',
'type' => 'style',
'src' => plugin_dir_url(__FILE__) . 'css/widget.css'
);
return $assets;
});
3. Runtime Execution Order #
Server-Side Execution #
1. wp_enqueue_scripts (priority 5)
└─ Register loader script
2. wp_enqueue_scripts (priority 999)
└─ Enqueue loader with localized data
3. wp_head (priority 1)
└─ Output resource hints (preconnect, dns-prefetch)
4. wp_head (priority 2)
└─ Output preload links
5. wp_head (priority 3)
└─ Output inline critical CSS
Client-Side Execution #
1. DOMContentLoaded event
└─ EWPDynamicAssetLoader initializes
2. Load critical assets (critical: true)
└─ Bypasses DOM check, loads immediately
3. Check DOM for selectors
└─ Load assets when selectors found
4. Setup MutationObserver
└─ Watch for dynamically added content
5. Setup IntersectionObserver
└─ Watch for lazy-loaded elements (lazy: true)
6. Periodic check (every 5 seconds)
└─ Fallback for missed mutations
4. Critical CSS Implementation #
Critical CSS is inlined in the <head> to eliminate render-blocking requests for above-the-fold content.
Basic Critical CSS #
add_filter('ewp_register_dynamic_assets', function($assets) {
$assets[] = array(
'handle' => 'hero-section',
'selector' => '.hero',
'type' => 'style',
'src' => get_template_directory_uri() . '/css/hero.css',
'critical' => true,
'critical_css' => '
.hero {
min-height: 100vh;
display: flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.hero-title {
font-size: 3rem;
color: white;
}
'
);
return $assets;
});
Output in HTML #
<style id="ewp-critical-css" data-assets="hero-section,navigation">
/* hero-section */
.hero {
min-height: 100vh;
display: flex;
align-items: center;
}
/* navigation */
.main-nav {
position: fixed;
top: 0;
}
</style>
Filter Critical CSS #
add_filter('ewp_dynamic_assets_critical_css', function($critical_css) {
// Add global critical CSS
$critical_css['global'] = '
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
';
return $critical_css;
});
5. Public API Methods #
The loader exposes public methods via window.EWPDynamicAssetLoader for manual control.
Check All Assets After AJAX #
// After AJAX call loads new content
fetch('/api/load-content')
.then(response => response.text())
.then(html => {
document.getElementById('content').innerHTML = html;
// Trigger asset check
window.EWPDynamicAssetLoader.checkAssets();
});
Load Specific Asset #
// Load asset by handle if selector exists
window.EWPDynamicAssetLoader.loadAssetByHandle('my-widget');
Force Load Asset #
// Force load without DOM check (use with caution)
window.EWPDynamicAssetLoader.forceLoadAsset('emergency-script');
Check Asset Status #
// Check if asset is loaded
if (window.EWPDynamicAssetLoader.isAssetLoaded('my-widget')) {
console.log('Widget is ready!');
}
// Get all loaded assets
const loaded = window.EWPDynamicAssetLoader.getLoadedAssets();
// ['ewp-search', 'my-widget']
// Get all registered assets
const registered = window.EWPDynamicAssetLoader.getRegisteredAssets();
6. Advanced Configuration #
Script with Dependencies #
$assets[] = array(
'handle' => 'my-app',
'selector' => '.my-app',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/app.js',
'dependencies' => array('jquery', 'wp-api'),
'defer' => true
);
ES6 Module #
$assets[] = array(
'handle' => 'my-module',
'selector' => '.my-component',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/module.js',
'module' => true // Loads as type="module"
);
Localized Script #
$assets[] = array(
'handle' => 'my-widget',
'selector' => '.my-widget',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/widget.js',
'localize' => array(
'objectName' => 'myWidgetData',
'data' => array(
'apiUrl' => rest_url('my-plugin/v1/'),
'nonce' => wp_create_nonce('wp_rest'),
'strings' => array(
'loading' => __('Loading...', 'my-plugin')
)
)
)
);
Lazy Loading #
$assets[] = array(
'handle' => 'gallery',
'selector' => '.gallery',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/gallery.js',
'lazy' => true // Uses Intersection Observer
);
Resource Hints #
$assets[] = array(
'handle' => 'google-maps',
'selector' => '[data-map]',
'type' => 'script',
'src' => 'https://maps.googleapis.com/maps/api/js?key=KEY',
'async' => true,
'resource_hints' => array(
'preconnect' => array(
'https://maps.googleapis.com',
'https://maps.gstatic.com'
)
)
);
7. Performance Configuration #
Global Lazy Load Settings #
// Disable lazy loading globally
add_filter('ewp_dynamic_assets_lazy_load', '__return_false');
// Adjust Intersection Observer threshold
add_filter('ewp_dynamic_assets_intersection_threshold', function() {
return 0.25; // Load when 25% visible
});
// Adjust root margin
add_filter('ewp_dynamic_assets_root_margin', function() {
return '100px'; // Load 100px before viewport
});
Global Resource Hints #
add_filter('ewp_dynamic_assets_resource_hints', function($hints) {
$hints['preconnect'][] = 'https://cdn.example.com';
$hints['dns-prefetch'][] = 'https://analytics.example.com';
return $hints;
});
8. Event Listeners #
Asset Loading Events #
// Listen for asset loading start
document.addEventListener('ewp_dynamic_asset_loading', function(e) {
console.log('Loading:', e.detail.asset.handle);
});
// Listen for asset loaded
document.addEventListener('ewp_dynamic_asset_loaded', function(e) {
console.log('Loaded:', e.detail.asset.handle);
console.log('Success:', e.detail.success);
});
9. Context-Specific Loading #
Control where assets load using the context parameter.
Frontend-Only Asset #
$assets[] = array(
'handle' => 'public-slider',
'selector' => '.slider',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/slider.js',
'context' => 'frontend' // Only loads on public pages
);
Admin-Only Asset #
$assets[] = array(
'handle' => 'admin-dashboard-widget',
'selector' => '.admin-dashboard-widget',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/admin-widget.js',
'context' => 'admin', // Only loads in WordPress admin
'localize' => array(
'objectName' => 'adminWidgetData',
'data' => array(
'restUrl' => rest_url('my-plugin/v1/'),
'nonce' => wp_create_nonce('wp_rest')
)
)
);
Both Contexts (Default) #
$assets[] = array(
'handle' => 'universal-component',
'selector' => '.universal-component',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/universal.js',
'context' => 'both' // Loads everywhere (default if omitted)
);
10. Debug Mode #
Debug logging is automatically enabled when WP_DEBUG is true.
Enable Debug Mode #
// In wp-config.php
define('WP_DEBUG', true);
Console Output Example #
[EWP Dynamic Assets] Initializing Dynamic Asset Loader {assetsCount: 3}
[EWP Dynamic Assets] Loading critical assets {count: 1, handles: ['hero-section']}
[EWP Dynamic Assets] Checking DOM for assets
[EWP Dynamic Assets] Selector found: .my-widget {handle: 'my-widget'}
[EWP Dynamic Assets] Loading asset: my-widget {type: 'script', src: '...'}
[EWP Dynamic Assets] Script loaded: my-widget
Benefits:
- Zero performance impact in production (no logging when
WP_DEBUGis false) - Track asset loading lifecycle in development
- Debug selector issues and timing problems
- Available globally for developer scripts via
EWPDynamicAssetLoader.log()
Using Debug Logger in Your Scripts #
The static log method is globally available for use in your dynamically loaded scripts:
// Static method - available globally
EWPDynamicAssetLoader.log('My script initialized');
// With data
EWPDynamicAssetLoader.log('Form submitted', {
formId: 'contact-form',
fields: 5
});
// In event handlers
document.addEventListener('ewp_dynamic_asset_loaded', (e) => {
EWPDynamicAssetLoader.log('Asset loaded event', e.detail);
});
Using Instance Methods #
For instance methods (like checkAssets()), use window.EWPDynamicAssetLoader.instance:
// After AJAX content load
const loader = window.EWPDynamicAssetLoader.instance;
// Check for new assets
loader.checkAssets();
// Load specific asset
loader.loadAssetByHandle('my-widget');
// Check if loaded
if (loader.isAssetLoaded('my-widget')) {
console.log('Widget is ready!');
}
The logger automatically respects WP_DEBUG – no output in production.
11. Best Practices #
Critical CSS Guidelines #
- Keep it minimal – Only above-the-fold content (< 14KB)
- Mobile-first – Prioritize mobile viewport styles
- No duplication – Don’t repeat styles from main CSS
- Compress – Always minify critical CSS
- Test – Use Lighthouse to verify FCP/LCP improvements
Selector Best Practices #
- Be specific – Use unique class names (
.my-widgetnot.widget) - Avoid generic – Don’t use
.container,.wrapper, etc. - Use data attributes –
[data-component="gallery"]for clarity - Test selectors – Verify they match intended elements only
Performance Tips #
- Combine assets – Group related scripts/styles under one selector
- Use lazy loading – For below-the-fold content
- Set critical flag – For above-the-fold assets
- Monitor metrics – Use Performance API methods
12. Extracting Critical CSS #
Manual Method (Chrome DevTools) #
- Open page in Chrome
- Press
Cmd+Shift+P(Mac) orCtrl+Shift+P(Windows) - Type “Show Coverage” and press Enter
- Click reload button in Coverage tab
- Identify used CSS for above-the-fold content
- Copy used styles to
critical_cssparameter
Automated Tools #
# Using Critical package
npm install critical --save-dev
const critical = require('critical');
critical.generate({
inline: false,
base: 'dist/',
src: 'index.html',
target: 'css/critical.css',
width: 1300,
height: 900,
dimensions: [
{ width: 375, height: 667 },
{ width: 1920, height: 1080 }
]
});
13. Debugging #
Check Loaded Assets #
// In browser console
console.log(window.EWPDynamicAssetLoader.getLoadedAssets());
console.log(window.EWPDynamicAssetLoader.getRegisteredAssets());
View Performance Metrics #
// In browser console
window.EWPDynamicAssetLoader.logPerformanceSummary();
// Output:
// EWP Dynamic Asset Loader - Performance Summary
// my-widget: 45.23ms
// gallery: 23.45ms
Verify Critical CSS #
View page source and look for:
<style id="ewp-critical-css" data-assets="hero-section,navigation">
/* Critical CSS content */
</style>
14. Common Issues #
Issue: Asset not loading #
Solution: Check selector specificity
// Test selector in console
document.querySelector('.my-widget'); // Should return element
Issue: Script errors with global functions #
Solution: Don’t use module: true for traditional scripts
// Wrong - isolates scope
'module' => true
// Correct - global scope
'module' => false // or omit
Issue: Critical CSS too large #
Solution: Only include truly critical styles
// Wrong - includes footer
'critical_css' => '
.hero { ... }
.footer { ... } // Remove this
'
// Correct - only above-fold
'critical_css' => '
.hero { ... }
'
15. Quick Reference #
Asset Parameters #
| Parameter | Type | Required | Description |
|---|---|---|---|
handle |
string | Yes | Unique asset identifier |
selector |
string | Yes | CSS selector to check |
type |
string | Yes | ‘script’ or ‘style’ |
src |
string | Yes | Asset URL |
version |
string | No | Cache busting version |
context |
string | No | ‘frontend’, ‘admin’, or ‘both’ (default) |
dependencies |
array | No | Asset handles to load first |
module |
bool | No | Load as ES6 module (scripts) |
async |
bool | No | Load asynchronously (scripts) |
defer |
bool | No | Defer execution (scripts) |
in_footer |
bool | No | Load in footer (scripts) |
media |
string | No | Media query (styles) |
localize |
array | No | Localization data (scripts) |
critical |
bool | No | Load immediately |
critical_css |
string | No | Inline CSS (styles) |
preload |
bool | No | Add preload link |
resource_hints |
array | No | Preconnect/DNS-prefetch |
Public API Methods #
| Method | Description |
|---|---|
checkAssets() |
Check all assets for new DOM elements |
loadAssetByHandle(handle) |
Load specific asset if selector exists |
forceLoadAsset(handle) |
Force load asset (bypasses DOM check) |
isAssetLoaded(handle) |
Check if asset is loaded |
getLoadedAssets() |
Get array of loaded handles |
getRegisteredAssets() |
Get array of registered assets |
getPerformanceMetrics() |
Get performance timing data |
EWPDynamicAssetLoader.log(message, data) |
Static debug logger (respects WP_DEBUG) |
Filter Hooks #
| Hook | Description |
|---|---|
ewp_register_dynamic_assets |
Register assets |
ewp_dynamic_assets_resource_hints |
Add global resource hints |
ewp_dynamic_assets_preload |
Filter preload assets |
ewp_dynamic_assets_critical_css |
Filter critical CSS output |
16. Performance Validation #
Target Metrics #
- FCP (First Contentful Paint): < 1.8s
- LCP (Largest Contentful Paint): < 2.5s
- TTI (Time to Interactive): < 3.8s
- Critical CSS Size: < 14KB
- Unused CSS: < 20%
Testing Tools #
- Google Lighthouse – Overall performance score
- WebPageTest – Detailed waterfall analysis
- Chrome DevTools – Coverage and Performance tabs
- PageSpeed Insights – Real-world data
17. Migration from Traditional Loading #
Before (Traditional) #
function my_plugin_enqueue_scripts() {
wp_enqueue_script('my-widget', plugin_dir_url(__FILE__) . 'js/widget.js');
wp_enqueue_style('my-widget', plugin_dir_url(__FILE__) . 'css/widget.css');
}
add_action('wp_enqueue_scripts', 'my_plugin_enqueue_scripts');
After (Dynamic) #
add_filter('ewp_register_dynamic_assets', function($assets) {
$assets[] = array(
'handle' => 'my-widget',
'selector' => '.my-widget',
'type' => 'script',
'src' => plugin_dir_url(__FILE__) . 'js/widget.js'
);
$assets[] = array(
'handle' => 'my-widget-styles',
'selector' => '.my-widget',
'type' => 'style',
'src' => plugin_dir_url(__FILE__) . 'css/widget.css'
);
return $assets;
});
Document version 1.0 – Last updated: January 2026