PHPWordPressWooCommerce

Optimizing Variation Queries in WooCommerce

When working on a WooCommerce project with a heavy focus on variable products, I found that the helper function get_available_variations() worked well, but came with a large performance impact. A default WooCommerce shop is set up to expose simple product information on the top level shop archive, while variations of the product would be exposed on the single product pages. A recent project of mine required the majority of variation information on the archive, and I found that even when loading ~16 products per page, the page load speed became incredibly sluggish using the WooCommerce provided function.

When diving into the plugin code around the function, I found that the information returned by this function was robust, but too much for my use case. A total of 22 key/value pairs is returned for each variation. The majority of these key/values are the result of a query. As you can imagine, calling this for each product on the shop archive can exponentially increase the number of queries on a page load. As I only needed some meta information about variations, I was able to achieve a more linear number of queries (1 query to get instock variations, +1 / variation).

The function expects a product object to be passed in. This is usually available on WooCommerce templates via the global $product; declaration. You can otherwise create this product object by calling $product = wc_get_product( $product_id );.

/**
 * Acts similar to $product->get_available_variations()
 * but retrieves less information and performs less queries.
 */
function woo_get_variations( $product = null ) {
    global $wpdb;

    if ( is_null( $product ) ) {

        // Exit, not a product.
        if ( get_post_type( get_the_ID() ) !== 'product' ) {
            return null;
        }

        // Use current loop product.
        $product = wc_get_product( get_the_ID() );
    }

    // Return value.
    $variations = [];

    // Unique transient key per product ID.
    $transient_key = sprintf( "product_%s_variations", $product->get_id() );

    // Check for transient before running more expensive queries.
    if ( false === ( $variations = get_transient( $transient_key ) ) ) {

        $children = $product->get_children();
        $children_IN = implode( ",", $children );

        $post_table = $wpdb->prefix . "posts";
        $woo_table = $wpdb->prefix . "wc_product_meta_lookup";

        // Query using WooCommerce Product Meta table rather than post meta.
        $available_variations = $wpdb->get_results(
            "SELECT posts.ID
            FROM $post_table AS posts
            LEFT JOIN $woo_table AS product_meta
                ON product_meta.product_id = posts.ID
            WHERE posts.ID IN ( $children_IN )
            AND posts.post_status = 'publish'
            AND posts.post_type = 'product_variation'
            AND product_meta.stock_status = 'instock'
            "
        , ARRAY_A );

        $available_variations = wp_list_pluck( $available_variations, 'ID' );

        /**
         * We need the following information on each variation.
         * Customize this for your own usage.
         * ID               - id
         * Color            - color
         * Width            - width
         * Size             - size
         * Image ID         - image_id
         * Stock            - stock
         */
        foreach ( $available_variations as $variation ) {

            // Fetch all postmeta for variation in a single query.
            $v_meta = get_post_meta( $variation );

            if ( $v_meta ) {

                // Append information to return value.
                // Customize these metas for your own use case.
                $variations[] = [
                    'id'       => $variation,
                    // See following helper function `safe_meta_get()`.
                    'color'    => safe_meta_get( $v_meta, 'attribute_pa_color' ),
                    'width'    => safe_meta_get( $v_meta, 'attribute_pa_width' ),
                    'size'     => safe_meta_get( $v_meta, 'attribute_pa_size' ),
                    'stock'    => safe_meta_get( $v_meta, '_stock' ),
                    'image_id' => safe_meta_get( $v_meta, '_thumbnail_id' ),
                ];
            }
        }

        set_transient( $transient_key, $variations, DAY_IN_SECONDS * 2 );
    }

    return maybe_unserialize( $variations );
}

/**
 * Reduce repitition in woo_get_variations().
 */
function safe_meta_get( $meta, $key ) {
    return ( isset( $meta[ $key ] ) && count( $meta[ $key ] ) ) ? $meta[ $key ][0] : null;
}