Skip to content
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

How to invalidate the cache in JS? #352

Closed
bakura10 opened this issue Jan 6, 2025 · 13 comments
Closed

How to invalidate the cache in JS? #352

bakura10 opened this issue Jan 6, 2025 · 13 comments

Comments

@bakura10
Copy link

bakura10 commented Jan 6, 2025

Hello,

I am exploring speculation rules in context of a Shopify theme (e-commerce) and so far, I have had great success. Performance is really nice and most of the limitations I had with Instant.Page are fixed by this system.

However, similar to Instant.Page, I am facing an issue that is pretty common in e-commerce:

  1. Open a product page.
  2. Hover another page that will trigger prerendering.
  3. Add a product to the cart.
  4. Navigate to the page that was pre-rendered.

Due to that, the cart content / cart count will show the value before the product has been added to cart. Ideally, we would need a JavaScript API to allow invalidate all or part of the cache, that we could call when a product has been added to the cart, for instance.

For now, the only workaround I've found would be to add a flag to local storage when a product has been added to cart, and use the document.prerendering to force a reload of the page, but that's defeating a bit the purpose.

Thanks.

@tunetheweb
Copy link

If you remove the speculation rules from the page then this removes the pre-prerendred page.

You can also re-prerender them by re-adding the speculation rules in a new task (or new microtask). Note this must be in a separate task (using setTimeout for example) or microtask (using queueMicrotask) to allow the browser to realise the rule has been removed. It cannot be removed and re-added in the same JavaScript task or the browser will not see the change.

A function like this can refresh all speculated pages and it could be called when add to basket is used:

function RefreshPrerenders() {
    const speculationScripts =
      document.querySelectorAll('script[type="speculationrules"]');
    for (speculationScript in speculationScripts) {
        // Get the current rules as JSON text
        const ruleSet = speculationScript.textContent;
        // Remove the existing script to reset prerendering
        speculationScript.remove();
        // Create a new speculation rules script in a new task
        // (to give the browser time to cancel the older speculation)
        // Note: queueMicrotask also works here.
        setTimeout( () => {
            const newScript = document.createElement('script');
            newScript.type = 'speculationrules';
            newScript.textContent = ruleSet;
            console.log(newScript);
            // Append the new script back to the document
            document.body.appendChild(newScript);
        }, 0);
    }
}

A better pattern is to recheck current state on page activation and update. This will allow the previous prerender not to be wasted (or have to be re-prerendered). If this is also done on page visibility changes then it would also solve this problem for those of us that use multiple tabs even when prerender is not used.

@bakura10
Copy link
Author

bakura10 commented Jan 6, 2025

Thanks for the answer. This looks like a rather complex solution for something that should be simpler. Can’t we have something like document.invalidateSpeculation(optionalPath) ?

@tunetheweb
Copy link

Well that JS snippet is to remove and re-add all speculation rules so is a bit more than just invalidating it. To invalidate all you can just do:

document.querySelectorAll('script[type="speculationrules"]').forEach(element => element.remove());

Which is a bit simpler than my snippet (but also a bit more destructive in that all rules will now no longer exist and so new speculations cannot be rerun).

But not sure what exactly you mean by "invalidate"?

  • Do you mean cancel? Which is basically what that smaller snippet does.
  • Or cancel and automatically re-speculate? Which is trickier as the page does not know about whether speculations have been triggered or not.
  • Or cancel, but allow re-speculation if the user triggers it again (e.g. by hovering over the link again)? Which is basically what my larger snippet does. But yes, I agree this could be made more ergonomic.

@bakura10
Copy link
Author

bakura10 commented Jan 6, 2025

Yes, your third situation is what we are trying to do. So cancel all speculation but keep the rules and re trigger them based on the selected behavior.

I've tried the snippet and it works as you said but it really feels a bit hacky and hardly discoverable (especially wrapping it around a setTimeout).

I feel this use case is common enough (at least all e-commerce websites will need it) to deserve a cleaner API.

@tunetheweb
Copy link

and hardly discoverable (especially wrapping it around a setTimeout).

Agreed. I'm working on a new post as we speak to document this and make this more well-known.

Will let others here comment on the addition of a new API. As I say, I do agree this could be made more ergonomic, but not sure of the priority of this work.

@domenic
Copy link
Collaborator

domenic commented Jan 7, 2025

The ideal solution here is to keep your pages up to date while they're being prerendered, or rendered in a second tab, using solutions like BroadcastChannel. See some discussion in MDN.

Otherwise, removing the speculation rule is indeed the best solution.

@bakura10
Copy link
Author

bakura10 commented Jan 7, 2025

I was discussing with a Shopify engineer about this, as there are interest to have support of speculation rules in Shopify natively. Due to this invalidation issue though, their idea would be to use a conservative approach to limit the risk (but it would not completely eliminate it though), but after testing it, while this reduce the TTFB, it's near the nearly instant experience of more aggressive approach.

What about allowing a response to return special HTTP headers indicating the browser to invalidate the cache? For instance,in Shopify context, the cache would need to be invalidated whenever the customer interact with the cart (so when a new line item is edited, removed or added).

Shopify could eventually configure their server to make all those /cart/* Ajax calls returning a special header (Speculation-Rules-Invalidate-Path for instance) that would automatically remove the prerendered/prefeteched page matching those path.

This would provide a clean way to handle this problem. And the browser could automatically invalidate the cache of all tabs sharing the same domain.

@domenic
Copy link
Collaborator

domenic commented Jan 8, 2025

It's worth considering, but my initial impression is that's a lot of complexity to add to the feature (now the server can control the speculation rules cache, separately from the HTTP cache!?). And it's just another way to do something that's already possible.

Can you explain why the existing invalidation abilities are not possible for your site?

@bakura10
Copy link
Author

bakura10 commented Jan 8, 2025

I Will let Shopify clarify (ping @krzksz) but from what I understand Shopify would like to integrate this natively to all existing Shopify stores (a kind of platform level managed rules).

Invalidation would happen whenever the user interacts with the cart (add a product to cart, remove a product to cart, edit an existing quantity…). Based on your approach, there would be two ways:

  • Shopify should ask merchants to update their theme or edit their code to implement the approach you have suggested (of course, this approach is unfeasible in reality)
  • Shopify should manage it as part of their managed implementation of speculation rules. To do that, Shopify would have to intercept any fetch call to see if they are Ajax calls interacting with the cart. Intercepting fetch is really hacky, and with such a huge ecosystem as Shopify, monkeypatching fetch native implementation to capture calls will probably break thousands of stores. So I don’t see your suggested approach being feasible in Shopify situation.

the response header approach brings a clean solution that would allow Shopify to deploy this and improving performance of millions on stores, without any drawback.

E-commerce where speed is essential appears to be THE perfect use case of speculation rules, so not having a solution for that sounds to me like a major flaw in the speculation rules API design.

@domenic
Copy link
Collaborator

domenic commented Jan 17, 2025

I forgot to say, thanks for that detailed comment. What you explained makes sense. In general, we've found that it's worthwhile to add features to help balance the platform / individual website responsibilities, instead of requiring them to coordinate or interfere with each others' code. That's what's driven work on the Speculation-Rules header, as well as #336.

We're considering extending the Clear-Site-Data header with this ability.

@bakura10
Copy link
Author

Thanks a lot for considering a solution to this problem. I've never heard of this header but this sounds like a nice idea. I think that we can start with a simple solution clearing all the speculation rules (after re-thinking it in context of Shopify for instance, I actually can't think of a use case where I would like to invalidate just a part of the rules; as adding to the cart can actually modify the HTML of every possible page).

@domenic
Copy link
Collaborator

domenic commented Jan 31, 2025

Issue triage: I have captured the ideas around clear-site-data in this new issue: #357.

Let me close this issue, as we've departed a decent ways from the OP, so starting fresh there seems cleaner. But if people still believe JS control is a good idea, then we can reopen or create a separate issue.

@domenic domenic closed this as completed Jan 31, 2025
@bakura10
Copy link
Author

bakura10 commented Feb 1, 2025

Thanks for moving on this. With this approach I don’t see any need for Javascript based invalidation :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants