Journal

Adding JavaScript to WordPress the Right Way: Hooks, Enqueuing, and Caching

How to inject a third-party script into WordPress without breaking caching or PageSpeed - the wp_footer hook, wp_add_inline_script, async/defer, and how caching plugins like WP Rocket treat dynamically injected widgets.

BBaidyanathMay 19, 20267 min readUpdated June 10, 2026
Adding JavaScript to WordPress the Right Way: Hooks, Enqueuing, and Caching

There's a rite of passage for anyone who runs a WordPress store: you paste a widget snippet into the footer, it works, you install a caching plugin for that sweet PageSpeed score, and the widget quietly vanishes. Then you spend an evening learning more about JavaScript optimisation than you ever wanted to. This post is the shortcut - how to add a script the way WordPress wants you to, and how the popular caching plugins treat what you added.

Don't echo a raw script tag

The tempting move is to drop a <script> tag straight into header.php or a wp_head callback. It works right up until something else touches the page - a caching plugin, an optimisation plugin, or a theme update. WordPress has a dependency system for scripts, and bypassing it is what creates the conflicts.

Use one of two enqueue paths instead.

An external file belongs in wp_enqueue_script, loaded in the footer:

add_action('wp_enqueue_scripts', function () {
  wp_enqueue_script(
    'my-widget',
    'https://cdn.example.com/widget.js',
    array(),      // dependencies
    null,         // version (null = no query string)
    true          // load in footer
  );
});

An inline snippet - a config object, an init call - should be attached to an enqueued handle with wp_add_inline_script, never floated on its own:

add_action('wp_enqueue_scripts', function () {
  wp_enqueue_script('my-widget', 'https://cdn.example.com/widget.js', array(), null, true);
  wp_add_inline_script('my-widget', 'window.MyWidgetConfig = { siteKey: "pk_live_abc123" };', 'before');
});

The 'before' argument prints the config before the widget file, so the config exists by the time the widget runs. Attaching the inline snippet to the same handle is what keeps them together when a caching plugin starts reordering things - they move as a unit.

If you'd rather not touch enqueuing, the wp_footer hook is the low-ceremony option for a self-contained snippet:

add_action('wp_footer', function () {
  ?>
  <script async src="https://cdn.example.com/widget.js" data-site-key="pk_live_abc123"></script>
  <?php
}, 20);

The footer placement matters: a chat or marketing widget is never on the critical rendering path, so loading it late keeps it from delaying first paint.

async vs defer

For a self-contained third-party widget that doesn't depend on other scripts, async is the right attribute - it downloads in parallel and runs as soon as it's ready. defer is for scripts that must run in document order after the HTML is parsed (anything with dependencies). If you're not sure, a standalone widget wants async.

How caching plugins treat injected widgets

Here's where the widget "disappears". Caching plugins don't just cache HTML - they optimise JavaScript, and three optimisations in particular trip up widgets.

File combining / concatenation. Older optimisation modes merge JS files into one bundle. A third-party widget served from another domain can't be combined, and forcing it usually breaks it. Modern setups (HTTP/2 and up) mostly drop combining anyway, but if it's on, exclude the widget.

Deferring all JavaScript. Blanket defer changes execution order. A widget that expected its inline config to run first can find the config arriving late. Keeping the config attached to the same handle (the wp_add_inline_script pattern above) is most of the protection here.

Delay JS until interaction. This is the big one. "Delay JavaScript execution" holds non-critical scripts until the first scroll, tap, or mouse move - excellent for scores, because nothing non-essential runs during the initial paint. But a proactive widget - one meant to greet the visitor or offer help unprompted - looks broken if it's waiting for an interaction that might never happen. The visitor who would have engaged never sees it.

The fix in all three cases is the same: exclude the widget's script from optimisation, not disable caching. The page stays cached and fast; one script is left alone.

WP Rocket

WP Rocket is the common case, so concretely: add the widget's URL (or a recognisable substring of it) to File Optimization → Exclude JavaScript Files, and - importantly - to the "Delay JavaScript execution" exclusion list if the widget is proactive. WP Rocket already excludes the WooCommerce cart and checkout pages from caching by default, so transactional pages are safe; it's the always-on widgets on cached pages that need the exclusion.

For genuinely passive widgets - something the user has to click to open - you can leave the delay in place and get the PageSpeed benefit for free. The rule of thumb: does the widget need to do something before the user touches the page? If yes, exclude it from delay. If no, let it ride.

W3 Total Cache and LiteSpeed

The concepts map across plugins even when the menus don't. W3 Total Cache exposes minify and "Eliminate render-blocking JS" options with their own exclusion fields; LiteSpeed Cache has a "JS Excludes" / "JS Deferred Excludes" pair under Page Optimization. Same move: find the optimisation that's eating your widget, add the script to that optimisation's exclusion list.

Where to exclude a widget script, by caching plugin
PluginOptimisation that breaks proactive widgetsWhere to exclude it
WP RocketDelay JavaScript executionFile Optimization -> Delay JS exclusion list
W3 Total CacheRender-blocking JS / minifyMinify -> JS exclusions
LiteSpeed CacheJS defer / delayPage Optimization -> JS Excludes / JS Deferred Excludes
AutoptimizeAggregate + defer JSJS, CSS & HTML -> Exclude scripts from Autoptimize

A quick verification pass

After any caching change, don't trust the dashboard - trust the page. Open the live site in a private window (so you're served the cached version, not your logged-in bypass), confirm the widget appears and behaves, and check the browser console for ordering errors. For a proactive widget, specifically confirm it fires without you interacting first. Two minutes here saves the "it works on my screen" support ticket later.

How Yokaify loads on WordPress

Yokaify ships as a single async script tag pointed at the hosted widget file:

<script async src="https://cdn.yokaify.com/widget.js" data-site-key="YOUR_SITE_KEY"></script>

The Yokaify WordPress plugin adds that line once via the wp_footer hook so you don't hand-edit theme files, and it registers the script's URL on WP Rocket's delay-exclusion list automatically - because Yokaify is a proactive widget, and a proactive widget that waits for a click it never gets is just an expensive comment in your page source. Because it's one external file, the caching plugin caches the page around it and never has to touch the widget itself.

Further reading

Frequently asked questions

Enqueue it from functions.php with wp_enqueue_script (external file) or wp_add_inline_script (inline snippet attached to a handle). Avoid raw <script> tags in the template.

Last updated June 10, 2026.