Why does admin-ajax load slow and what are ways to speed it up?

Possible solution

I think a part of your issue is the \WP_Query call – querying the database isn’t “cheap” with regards to making fast, efficient AJAX calls, but you also can’t really avoid it in your situation.

One approach you could take would be caching the results of the query for some amount of time, at least in that case you would have one initially slow response for a category, and then rapid responses for subsequent queries.

function educaforma_filter_ajax(){
    $category_filter = "";

    if(isset($_POST['category'])){
        if($_POST['category'] == "todos"){
            $category_filter = "";
        }
        else{
            $category_filter = $_POST['category'];
        }
    }

    $output = "";
    $params = array(
        'product_cat'    => $category_filter,
        'posts_per_page' => 15,
        'post_type'      => 'product',
    );

    // Create a cache key based on the parameters and try to get a transient.
    $cache_key      = "educaforma_filter_results_" . implode( '_', $params );
    $cached_results = get_transient( $cache_key);

    if ( ! empty( $cached_results ) ) {
        echo $cached_results;
        exit;
    }

    $wc_query = new WP_Query($params);
    $count = $wc_query->post_count;
    $cont_post = 0; 
    if ($wc_query->have_posts()) : 
        while ($wc_query->have_posts()) :
            $wc_query->the_post(); 
    global $product;
    if($cont_post == 0): 

        $output .= "<div class="row">"; 

    elseif($cont_post%3 == 0):

        $output .= "</div>"; 
    $output .= "<div class="row">"; 

    endif;

    $output .= "<div class="col-md-4 col-sm-4">";
    // <!-- <div class="container-course"> -->
    $output .= "<a class="list-course" href="".$product->get_permalink()."">";

    // $output .= the_post_thumbnail();
    // $output .= $product->get_image(artiest,1,1); 
    $attachment_id = get_post_thumbnail_id( $product->id );
    $attachment = wp_get_attachment_image_src($attachment_id, 'full' );
    $output .= "<div class="image-product" style="position: relative;
    background-repeat: no-repeat;
    background-position: 50% 50%;
    background-size: cover;
    background-clip: border-box;
    box-sizing: border-box;
overflow: hidden;
height: 180px;
width: 100%;
       background-image: url(".$attachment[0].") !important;
       ">
           <div class="darken-image"></div>
           </div>";
       $output .= "<div class="separator"></div>";
       $output .= "<div class="container-course">";
       $output .= "<h3 class="title-course">";
       // $output .= the_title(); 
       $output .= $product->get_title();

       $output .= "</h3>";
       //the_excerpt(); 
       $output .= "<p class="title-attribute">Curso:  <span class="value-attribute">".$product->get_attribute('Curso')." </span></p>";
       $output .= "<p class="title-attribute">Inicio:  <span class="value-attribute">". $product->get_attribute('Inicio')." </span></p>";
       $output .= "<p class="title-attribute">FinalizaciĆ³n:  <span class="value-attribute">". $product->get_attribute('Finalizacion')." </span></p>";
       $output .= "<p class="title-attribute">Precio:  <span class="value-attribute">".$product->get_attribute('Precio')." </span></p>";
       $output .= "<p class="title-attribute">Avalado por: <span class="value-attribute"> ".$product->get_attribute('Avalado')." </span></p>";
       $output .= "</div>";
       $output .= "</a>";
       $output .= "</div>";

       $cont_post++; 
       endwhile; 
       wp_reset_postdata(); 
else:  
       $ouput.= "<li>";
       $ouput .= "No hay cursos";
       $output.="</li>";
       endif; 

    // Set the transient here!
    set_transient( $cache_key, $output, DAY_IN_SECONDS );
    echo $output;

}

In the above snippet, I added the following lines:

    // Create a cache key based on the parameters and try to get a transient.
    $cache_key      = "educaforma_filter_results_" . implode( '_', $params );
    $cached_results = get_transient( $cache_key);

    if ( ! empty( $cached_results ) ) {
        echo $cached_results;
        exit;
    }

This will create a “key” based on the parameters and attempt to find a matching transient with that name. Further below:

    // Set the transient here!
    set_transient( $cache_key, $output, DAY_IN_SECONDS );
    echo $output;

This sets the transient if we didn’t stop earlier when we found it. Now, the next time someone queries for the same category, the transient will be set and the results should be returned very quickly.

Notice also that the expiration is set for 24 hours (DAY_IN_SECONDS). You may want it shorter or longer, and depending on how frequently you update content, you may want some other means of invalidating the transients.

On query slowness

One reason your query may be slow is that it’s using your custom parameter, product_cat, which I’m assuming you’re parsing in some filter on the query somewhere and that probably relates to a term relationship. This means that WordPress is JOIN’ing several tables, and depending on your database, this can be a little slow.

Also, you’re delaying your response by 600 milliseconds with setTimeout – I know it’s not huge, but consider that you’re adding more than half a second to an already slow response.

I’m not sure what you can do to serve the content quicker aside from caching, at least without really digging into the internals of your site. Adding some form of visual feedback – like a loading spinner – could at least let users know that something is happening and will at least help the perception of your site’s load time (even if it doesn’t actually get any faster).