Products go into the cart, the counter increments — and checkout says the cart is empty. The most common cause is not caching but a speed plugin deferring the JavaScript block checkout depends on.
WooCommerce's block-based cart and checkout depend on WordPress's wp JavaScript object and the wp-* script packages. When a speed plugin or custom code defers every script indiscriminately, those packages execute in the wrong order, the console throws wp is not defined, and the block renders its empty-cart state. The fix is to exempt WordPress core and WooCommerce block scripts from the defer rule — not to clear the cart or the cache.
This failure has a distinctive signature:
The last two matter. It does not appear for an administrator because most speed plugins disable optimisation for logged-in users. That delays the site owner noticing anything: the failure only happens to real customers.
Most people try these in order, and none of them fixes it permanently:
The shared error is looking for the problem on the server. The server knows the cart perfectly well — the incrementing counter proves it.
Open checkout while logged out (a private window) and look at the browser console.
Uncaught ReferenceError: wp is not defined
at checkout-blocks.js:1
Uncaught TypeError: Cannot read properties of undefined (reading 'createElement')
If you see those two lines, the diagnosis is complete. The cart is fine, the server is fine; the JavaScript that draws the page dies before it can run.
Extra confirmation: look at the <script> tags in page source. If WordPress core scripts (wp-polyfill, wp-element, wp-data) also carry defer, the cause is certain.
WordPress scripts depend on each other, and that order is declared through wp_enqueue_script. Normally the browser preserves it.
Adding defer makes scripts run after HTML parsing. The specification says deferred scripts run in document order — but with dynamically added scripts, conditionally loaded bundles and inline code mixed in, that guarantee breaks in practice. In particular:
<script> blocks. That inline code runs immediately, while the wp-element it depends on has been deferred. The order inverts.The result: code trying to reach wp runs before it exists, throws, and the React-based block never mounts. Unmounted, it shows its default state — an empty cart.
When I traced this to root cause on a real site, the source was a blanket defer rule in a helper plugin attached to the theme: a few lines adding defer to every script on the site, with no exception list.
You do not remove the defer rule — you give it an exemption list. WordPress core and WooCommerce block scripts must not be deferred.
add_filter( 'script_loader_tag', function ( $tag, $handle ) {
// Handles that must never be deferred
$skip_prefixes = array( 'wp-', 'wc-', 'woocommerce' );
$skip_exact = array( 'jquery', 'jquery-core', 'jquery-migrate' );
foreach ( $skip_prefixes as $prefix ) {
if ( 0 === strpos( $handle, $prefix ) ) {
return $tag;
}
}
if ( in_array( $handle, $skip_exact, true ) ) {
return $tag;
}
// Never defer anything on cart and checkout
if ( function_exists( 'is_checkout' ) && ( is_checkout() || is_cart() ) ) {
return $tag;
}
if ( false !== strpos( $tag, ' defer' ) || false === strpos( $tag, ' src=' ) ) {
return $tag;
}
return str_replace( ' src=', ' defer src=', $tag );
}, 10, 2 );
With an off-the-shelf speed plugin the same logic goes through its settings: the JavaScript defer/combine section has an exclusion list. Add handles starting with wp-, wc- and woocommerce, and where possible exempt cart and checkout from optimisation entirely.
wp is not defined in particular must be gone."Your cart is empty" is usually not a cart bug but a script loading order bug. The server knows the cart; the JavaScript that would draw it cannot run.
The general rule: speed optimisation is never applied without exceptions. Blanket defer, combine or delay rules must exclude WordPress core scripts and the payment flow. Speed should not be paid for in sales.
The server almost certainly knows the cart correctly, but the JavaScript that renders it cannot run. Block-based cart and checkout depend on WordPress's wp object; when a speed plugin defers every script, that object is not defined in time and the block renders itself as an empty cart.
Because most speed plugins disable caching and script optimisation for logged-in users. Browsing as an administrator, no optimisation is applied and no error occurs. This is why testing must always be done in a private window, logged out.
No. It can look fixed because the first load changes script timing, but the problem returns shortly after. The permanent fix is adding an exclusion list to the defer rule covering WordPress core and WooCommerce scripts.
No need. Deferral gives a real gain on pages outside the payment flow. What matters is not applying it without exceptions: exempt scripts starting with wp-, wc- and woocommerce, and disable optimisation entirely on cart and checkout.