kylephillips / favorites

Simple and flexible favorite buttons for any WordPress post type.
https://favoriteposts.com
224 stars 85 forks source link

Ability to reduce Admin-ajax.php calls? #77

Open contemplate opened 7 years ago

contemplate commented 7 years ago

We use this plugin on a popular membership site (5300+ members) hosted at WPEngine. The favorites functionality is essential to the member experience for them to favorite video content however WPEngine wants us to remove the plugin due to it's over use of admin-ajax calls. On two different days we logged admin-ajax calls for about 10 minutes and both days it was over 1000 calls. When we disabled this plugin it dropped to only a handful of calls in the 10 minute window.

WPEngine says the plugin has generated almost 15 million admin ajax calls in the last week. They deem this as unacceptable :(

Is there any way to force a page refresh when someone likes a post rather that trigger admin-ajax? Also we are fine if the user's favorite list requires refreshing the page rather than being updated by ajax.

renatonascalves commented 7 years ago

One solution would be to rewrite the favorite functionality with wp_rewrite with proper caching: https://vip.wordpress.com/documentation/wp_rewrite/

As of right now, there is no caching in admin-ajax, and your users are testing the limits of the server.

Unfortunately, I can't see how it could be reduced as every favorite interaction would be needed to record the information.

contemplate commented 7 years ago

Thanks for the suggestion @renatonascalves however in doing some further testing I disabled the Page Cache setting and the admin-ajax calls dropped significantly. It wasn't that users were pressing the favorites button like crazy but just pulling up and browsing the grid of posts was triggering an ajax after call for every post. The favorite mechanism seems to still work fine with this setting turned off and WP Engine is happy with us again.

kylephillips commented 7 years ago

I would like to add non-ajax requests to the form elements, but that doesn’t solve the problem of cached pages and favorite button statuses.

I’m curious, is disabling the page cache setting an issue with wp engine’s caching? I have at least one site running on their platform with the plugin installed and had to specifically ask for caching on the cookie to be disabled, but haven’t run into your issue (with that many users).

contemplate commented 7 years ago

WP Engine has a global setting which will create a log file to record the admin-ajax calls which helped me test the difference between the page cache setting being on or off:

define( 'WPE_MONITOR_ADMIN_AJAX', true );

We don't use the anonymous favorites at all so don't need the cookie or sessions to be stored. But it could be that the page where we mostly use the favorites (the membership dashboard) is excluded from their cache. I can't remember where to find which pages we are excluding.

CHEWX commented 6 years ago

I think this is more of an issue with WP Engine and supporting large scale websites.

WooCommerce run admin-ajax.php and we ran into issue with a client that sold a hell of a lot of stuff, so every item being added to the cart was triggering a request which in turn added load to the server.

WPE's response was pretty crappy saying "pages need to be cached", however, that doesn't help. We ended up paying big bucks for one of their top tiers through sale periods and then migrating back down in low season.

I'm not sure whether WP-API vs admin-ajax.php will have much of a difference? Would be an interesting test. But until WooCommerce and their man-power find a solution, then I don't think much can change other than finding a host where tiny ajax requests don't kill the server.

@contemplate P.S, Triggering on page reload will not work, as PHP is not run due to pages being cached, I assume you have the favourite button on most pages, so you wouldn't want to turn off the cache on most pages. And to see which pages you have excluded, log into the WPE dashboard and click Advanced. You will see a 'Cache Exclude' section.

n0099 commented 1 year ago

I've created a service worker to cache the request to admin-ajax.php by a minute, it will issue the request by the first time, and cache its response into browser's IndexedDB, next time requesting it will no more real requests being hit ur server until the default one minute expires reached, feel free to use and modify it in ur site:

  1. Create a file /sw.js (/ means relative to your wordpress root url, u may use nginx to url-rewrite its real file path) with the following content:

    // https://github.com/kylephillips/favorites/issues/77
    const fetchWithCache = async requestCloned => {
    // https://alex-goff.medium.com/storing-data-with-indexeddb-for-service-workers-and-pwas-2da9d2ef30e2
    const getIndexedDBStore = cb => {
        const db = self.indexedDB.open('sw', 1);
        db.onsuccess = e => {
            const storeName = 'favorites_array';
            cb(e.target.result.transaction(storeName, 'readwrite').objectStore(storeName));
        };
        db.onupgradeneeded = e => e.target.result.createObjectStore("favorites_array");
    };
    const putIntoStore = (key, value) => getIndexedDBStore(store => store.put(value, key));
    const getFromStore = key => new Promise(reslove => getIndexedDBStore(store => {
        const request = store.get(key);
        request.onsuccess = () => reslove(request.result);
    }));
    const responseFactory = async () => new Response(await getFromStore('data'),
        // this will tell $.ajax({dataType: 'json'}) to parse the response body correctly, or it will be treated as a "text/plain" string
        // https://github.com/kylephillips/favorites/blob/6499a93f8427062c46bac372d3f7e00f32a29ae2/assets/js/favorites.js#L234
        { headers: {'Content-Type': 'application/json; charset=UTF-8' } });
    
    if (await getFromStore('expires') >= Math.floor(new Date().getTime() / 1000)) {
        return await responseFactory();
    }
    // main idea comes from https://gomakethings.com/how-to-set-an-expiration-date-for-items-in-a-service-worker-cache/
    // but Cache.put(request) doesn't work with POST method, so switch to the IndexedDB approach
    // also local/sessionStorage is not available in service worker
    try {
        // the request being passed to fetch() must get cloned first to prevent `Cannot construct a Request with a Request object that has already been used`
        const response = await fetch(requestCloned);
        const copy = response.clone();
        putIntoStore('data', await copy.text());
        putIntoStore('expires', Math.floor(new Date().getTime() / 1000) + 60); // one minute, feel free to change it
        return response;
    } catch (error) {
        console.error(error);
        return await responseFactory();
    }
    };
    self.addEventListener('fetch', e => {
    const { request } = e;
    // we must test the request body text within e.respondWith()
    // since e.waitUntil() will not hang up the default behavior of fetch event (issue the normal request)
    // being fired after any async operations (here is the request.text())
    // https://www.w3.org/TR/service-workers/#fetchevent-wait-to-respond-flag
    // https://stackoverflow.com/questions/47282211/using-both-respondwith-and-waituntil-in-a-fetch-event
    if (request.method === 'POST' && request.url === 'https://example.com/wp-admin/admin-ajax.php') {
        // fetch(request) will throws `Cannot construct a Request with a Request object that has already been used` if the request is not cloned
        e.respondWith((async () => await request.clone().text() === 'action=favorites_array' ? fetchWithCache(request) : fetch(request))());
    }
    });
  2. then register the sw.js in your theme's script like:

    $(() => {
    if (navigator.serviceWorker !== undefined) navigator.serviceWorker.register('/sw.js');
    });