Skip to content

Remove nocache headers from Cart page to enable bfcache (instant back/forward navigations) #58445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

westonruter
Copy link
Contributor

@westonruter westonruter commented Jun 1, 2025

Changes proposed in this Pull Request:

I found that nocache headers were being sent on the Cart page seemingly unnecessarily. This appears to have been done for the sake of the browser's back/forward cache (bfcache) holding onto a stale cart page which wouldn't reflect a change made from another page after hitting the back button. However, as can be seen in the current version of Chrome, this no longer seems to be the case. (See screen recording below.)

In particular, this is no longer necessary because there is a pageshow event listener running on the Cart page to update the cart whenever landing on the cart after a bfcache navigation. For example:

// Refresh when page is shown after back button (safari)
$( window ).on( 'pageshow' , function( e ) {
if ( e.originalEvent.persisted ) {
$( '.widget_shopping_cart_content' ).empty();
$( document.body ).trigger( 'wc_fragment_refresh' );
}
} );

This PR therefore improves performance of navigating to and from the cart page by allowing the page to be served from bfcache.

Finally, this PR ensures the original Cache-Control directives from nocache_headers() are preserved when additional nocache headers are set. Namely, in WP core changeset r55968 the use of private was introduced.


This includes a commit (33c6961) from #58444 to remove the obsolete Google Web Light support. Once that PR is merged, that change will be removed from this PR.

Screenshots or screen recordings:

Screen.Recording.2025-05-31.at.17.16.58.mov

How to test the changes in this Pull Request:

  1. Log out of WordPress (if logged-in, since Cache-Control: no-store is currently sent for authenticated users).
  2. Add products to the cart.
  3. Open DevTools to see the Back/Forward Cache test panel.
  4. Click on one of the products in the cart.
  5. From the mini-cart in the header, remove the first item in the cart.
  6. Click the back button.
  7. See that the Cart page was loaded instantly, that the page was loaded from bfcache, and see that the Cart is updated with the product removed.

Changelog entry

  • Automatically create a changelog entry from the details below.
  • This Pull Request does not require a changelog entry. (Comment required below)
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Fix - Fixes an existing bug
  • Add - Adds functionality
  • Update - Update existing functionality
  • Dev - Development related task
  • Tweak - A minor adjustment to the codebase
  • Performance - Address performance issues
  • Enhancement - Improvement to existing functionality

Message

Remove nocache headers from Cart page to enable bfcache

Changelog Entry Comment

Comment

@github-actions github-actions bot added plugin: woocommerce Issues related to the WooCommerce Core plugin. type: community contribution labels Jun 1, 2025
@westonruter
Copy link
Contributor Author

The pages for Checkout and My Account might also be suitable for this change, but I didn't test that they have pageshow event handlers in the same way that the Cart page does.

@senadir
Copy link
Member

senadir commented Jun 2, 2025

Thank you for this PR @westonruter, our perops team was looking into those headers and actually looking to add them to more pages, I will bring them to the discussion.

@westonruter
Copy link
Contributor Author

@senadir Adding no-store to more pages will degrade the performance of browsing a site. See https://web.dev/articles/bfcache#minimize-no-store

@senadir
Copy link
Member

senadir commented Jun 2, 2025

not more pages, just the whole cart/checkout pages, as we aggressively cache other pages and we don't want to do that for cart/checkout.

@westonruter
Copy link
Contributor Author

Oh good. So in other words, in addition to removing cart (as done in my PR here), you also intend to remove checkout from this line as well:

$page_ids = array_filter( array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ) ) );

@thingalon
Copy link

Removing the Chrome-specific stuff seems ok - and I particularly support the idea of preserving core's cache-control directives, especially "private".

But this will remove the "no-cache" directive for all visitors on the cart page. There are likely thousands of WooCommerce sites behind edge caches which rely on those headers to prevent caching shopping carts across users.

I think it would be better to add "private" to cache-control on cart and checkout pages instead of no-cache.

@westonruter
Copy link
Contributor Author

@thingalon By no-cache do you mean no-store?

This PR removes no-store for all visitors to the cart page, yes. However, because nocache_headers() is being called, core is sending the private directive which prevents edge caches from caching those responses.

Core added the private directive in WordPress 6.3, which is far before the 6.7 minimum supported version. So it appears that private will already be getting sent.

@thingalon
Copy link

thingalon commented Jun 3, 2025

@westonruter - this diff affects both the no-store and no-cache headers.

On a fresh temp WordPress / WooCommerce install:

$ curl -sI 'https://test-host.local/temp-site/cart/' | grep Cache-Control
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private

With the patch installed:

$ curl -sI 'https://test-host.local/temp-site-patched/cart/' | grep Cache-Control
(nothing)

The diff prevents nocache_headers() from being called on the cart page. Since shopping carts get served embedded in pages for unauthenticated users, it could cause full shopping carts to get caught up in reverse-proxy caches. :)

I think it'd be more appropriate to set Cache-Control: private on cart and checkout pages for unauthenticated users. i.e.: I think it'd be ok to drop the no-cache and no-store directives from cart pages, but I think we should retain private for the sake of reverse-proxy caches.

Thinking on it further: this will also bypass calling set_nocache_constants() on the cart page. The DONOTCACHEPAGE const defined there affects how common page-caching plugins like WP Rocket and WP Super Cache handle pages too.

@westonruter
Copy link
Contributor Author

westonruter commented Jun 3, 2025

@thingalon Oh yes, you're right. I confused myself because I also have in mind removing no-store from the default set of directives sent when calling nocache_headers(), leaving only private.

Update: See https://core.trac.wordpress.org/ticket/63636

So yes, Cache-Control: private is what needs to be added instead of calling nocache_headers() on the cart and checkout pages.

Copy link
Contributor

coderabbitai bot commented Jun 21, 2025

📝 Walkthrough

"""

Walkthrough

The caching prevention logic in WooCommerce was refactored by removing the old additional_nocache_headers filter and the parameterless prevent_caching method. A new prevent_caching method was introduced, hooked into the wp_headers filter, to manage cache-control headers specifically on cart, checkout, and my account pages, enabling bfcache by excluding no-store.

Changes

File Change Summary
plugins/woocommerce/includes/class-wc-cache-helper.php Refactored cache prevention: removed additional_nocache_headers filter and old parameterless prevent_caching; added new prevent_caching($headers) hooked to wp_headers; updated cache-control header logic to exclude no-store and remove user-agent checks.
plugins/woocommerce/changelog/58445-remove-cart-page-cache-control-no-store Removed "no-cache" headers from Cart page to enable browser back-forward cache (bfcache), improving navigation performance.

Sequence Diagram(s)

sequenceDiagram
    participant WP as WordPress
    participant WC as WC_Cache_Helper

    WP->>WC: Apply wp_headers filter (with $headers)
    WC->>WC: Check if WooCommerce page (cart, checkout, my account)
    alt If WooCommerce page
        WC->>WC: Define no-cache constants
        WC->>WC: Merge cache-control directives (private, no-cache, must-revalidate, max-age=0) into $headers (exclude no-store)
    else Not WooCommerce page
        WC->>WC: Return $headers unchanged
    end
    WC->>WP: Return modified $headers
Loading

Poem

🐇
A hop, a skip, the cache we tweak,
No more old headers, no more leak!
On carts and checkouts, bunnies prance,
With headers set, we take no chance.
Streamlined code, so neat and bright—
WooCommerce pages cache just right!

"""


📜 Recent review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d0d954 and e8da474.

📒 Files selected for processing (1)
  • plugins/woocommerce/changelog/58445-remove-cart-page-cache-control-no-store (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugins/woocommerce/changelog/58445-remove-cart-page-cache-control-no-store

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@westonruter
Copy link
Contributor Author

westonruter commented Jun 21, 2025

@thingalon OK, how about 3d0d954?

State Before After
Unauthenticated no-transform, no-cache, no-store, must-revalidate private, no-cache, must-revalidate, max-age=0
Authenticated no-transform, no-cache, no-store, must-revalidate no-cache, must-revalidate, max-age=0, no-store, private

In the authenticated state, the no-store directive is coming from wp_get_nocache_headers() sent from WordPress while logged-in. Otherwise, the other same directives from wp_get_nocache_headers() remain since they don't impact bfcache while also helping to ensure freshness. As noted above, I'm in the process of putting together a proposal to remove no-store from that. (Update: See Core-63636.)

The set_nocache_constants() call remains in place.

@thingalon
Copy link

@westonruter - that looks good to me, and seems like a clear improvement around caching headers. Thanks for working on this!

@senadir - can you please review it for merging into WooCommerce? :)

@westonruter
Copy link
Contributor Author

westonruter commented Jun 27, 2025

Here's a demo of the impact. While not being logged-in to WordPress and emulating a Slow 4G connection, compare entering a coupon on the cart page, then navigating to the shop, and then going back to the cart page. Notice how much faster the experience is when bfcache is enabled, and notice that the coupon dropdown retains its open state with the unsubmitted coupon still populating the field:

Before After
Screen.Recording.2025-06-27.at.14.33.17.mov
Screen.Recording.2025-06-27.at.14.34.36.mov

Granted, the speed effect is exaggerated here because I had DevTools open with cache disabled, besides emulating Slow 4G.

@senadir senadir self-requested a review July 7, 2025 07:57
Copy link
Contributor

github-actions bot commented Jul 7, 2025

Testing Guidelines

Hi ,

Apart from reviewing the code changes, please make sure to review the testing instructions (Guide) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

Reminder: PR reviewers are required to document testing performed. This includes:

  • 🖼️ Screenshots or screen recordings.
  • 📝 List of functionality tested / steps followed.
  • 🌐 Site details (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
  • 🔍 Any analysis performed, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.

@senadir senadir closed this Jul 7, 2025
@senadir senadir reopened this Jul 7, 2025
Copy link
Member

@senadir senadir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good to me, I left a couple of comments, one of them is slightly blocking unless you have which is about not using wp_get_nocache_headers if we're not longer hooking into nocache_headers filter.

*/
public static function additional_nocache_headers( $headers ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes, I'm a bit hesitant about deleting this public function outright but I can't see any usage for it publicly so I think we're good to go.

Comment on lines 59 to 68
// These directives are all included in `wp_get_nocache_headers()`, with the exclusion of `no-store` for the sake of bfcache.
$new_directives = array(
// Prevent caching the response in reverse proxies.
'private',

// Ensure freshness of the response but without `no-store` so that bfcache won't be disabled.
'no-cache',
'must-revalidate',
'max-age=0',
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why not use wp_get_nocache_headers and remove no-store from it instead of maintaining our own list here? that function also benefit from having a filter built-in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that makes sense.

How about this: we merge the headers from wp_get_nocache_headers() into $headers, and then if the user is not logged-in, we also remove no-store from Cache-Control. Then when https://core.trac.wordpress.org/ticket/63636 lands in core (hopefully) the no-store directive will also be removed for logged-in users automatically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to go with that!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 47f9b9c


if ( $set_cache ) {
$headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate';
}
return $headers;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR discussion mentioned adding back no-store for authenticated users, at which point are we doing that because I can't seem to see it? unless you're assuming being on cart/checkout/my-account equal being authenticated? which is technically true but doesn't cover all cases of being authenticated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding no-store or removing the directive?

Are you referring to this comment?

In https://core.trac.wordpress.org/ticket/63636 I'm working on removing no-store for authenticated users in core.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about this comment that includes no-store for authed users

#58445 (comment)

@senadir
Copy link
Member

senadir commented Jul 9, 2025

@westonruter I'm going on sabbatical by the end of this week; I'd love if we could get this merged ASAP so I don't need to hand it over. Let me know if you need help applying feedback.
Merging it this week will also land it in the WooCommerce 10.1 release coming out early August.

@westonruter
Copy link
Contributor Author

I will try to amend the PR tomorrow morning to do so, but I'll be AFK for the rest of the week.

westonruter and others added 2 commits July 10, 2025 09:11
…to remove/cart-page-cache-control-no-store
Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
Copy link
Member

@senadir senadir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, thank you @westonruter

@senadir senadir enabled auto-merge (squash) July 10, 2025 16:56
@senadir senadir merged commit 3d57338 into woocommerce:trunk Jul 11, 2025
68 of 70 checks passed
@github-actions github-actions bot added this to the 10.1.0 milestone Jul 11, 2025
@westonruter
Copy link
Contributor Author

I've opened a similar pull request for Jetpack: Automattic/jetpack#44322

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plugin: woocommerce Issues related to the WooCommerce Core plugin. type: community contribution
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants