“Issues” loading MANY featured images on a single page (custom template)

I have a page that loads a lot of featured items in a grid. There are up to 56 images loading, and it does have a bit of a delay.
This is the code as it runs now:

if( count( $postslist ) > 0 ) {
  foreach ($postslist as $post) : setup_postdata($post);
  ?> 
  <div class='oneCell'>
    <?php 
    $image = get_the_post_thumbnail($this_post->ID, 'full size'); 
    $imageSrc = substr($image, strpos($image, "src") + 5);
    $imageSrc = substr($imageSrc, 0, strPos($imageSrc, """));
    $finalImage = "<img class='lazy' src='/images/grey.png' data-original='";
    $finalImage .= $imageSrc . "' />";
    $lastImage = "<a href='";
    $lastImage .= catch_that_image();
    $lastImage .= "'>";
    $lastImage .= $finalImage;
    $lastImage .= "</a>";
    echo $lastImage;
    ?>
  </div>
  <?php 
  $currentCount = $currentCount + 1;
  endforeach;
}

I understand there are issues with the code, basically hitting the database once for every image in a loop, which is causing the slowness.

Read More

Can anyone help me out with a solution to speed up the loading? My thought is grabbing everything from the database all at once and displaying it from there, but I have yet to figure out how to handle that.

Thanks!

Edit:

Sorry, I should have explained this a bit more:

The get_the_post_thumbnail is asking for the fullsize image because the fullsize image is what it needs (189×189). The featured image in the post is that image, and will always be 189×189.

catch_that_image() just grabs the first (and only) image in the post body to be displayed when the user clicks on the featured image (thumbnail) using a js library (magnific-popup.js).

Edit 2:

It looks like my issue is database related. The calls are too slow to do one at a time (since there are so many). Is there a way to get everything back in one call then process it from there?

Related posts

Leave a Reply

2 comments

  1. The answer is eager loading or cache or both.

    Eager loading

    Have a look to this pseudo-code:

    $ids = get_ids_from_a_db_table();
    foreach ( $ids as $id ) {
      $row = get_row_from_foreign_table_using( $id );
      echo "Row title for the ID: $id is $row->title";
    }
    

    If the number of $ids is n than this simple code runs n+1 queries, the first to load the ids, then one for each id.

    You code is even worse, it run 2 queries for every id, one to get attachment post object, one to get attachemnt url (is WordPress, not you, really)

    So you run 2n+1 queries…

    Eager loading, referred to a db query, is the way you solve the n+1 (or 2n+1) queries problem by querying in only one db request all the data you need (normally is done using proper JOIN + WHERE )

    $data = get_data_from_joined_db_tables();
    foreach ( $data as $row ) {
      echo "Row title for the ID: $row->id is $row->title";
    }
    

    How do that in WordPress? You need two things:

    • add proper join and where clauses to merge the post meta table
    • filter the fields returned by WP_Query to include meta table fields

    Both can be done using query filters, here a class extending WP_Query that do that:

    class Thumb_Query extends WP_Query {
    
      protected static $args = array( 'nopaging' => TRUE );
    
      public function __construct( $args = '' ) {
        if ( empty( $args ) ) $args = self::$args; // defaults
        parent::__construct( $args );
      }
    
      public function get_posts() {
        add_filter('posts_clauses', array( __CLASS__, 'thumb_filters') );
        $results = parent::get_posts();
        remove_filter('posts_clauses', array( __CLASS__, 'thumb_filters') );
        return $this->parse_images( $results );
      }
    
      public static function thumb_filters( $pieces ) {
        $meta = $GLOBALS['wpdb']->postmeta;
        $posts = $GLOBALS['wpdb']->posts;
        $pieces['fields'] .= ", thumbs.meta_value as thumb_id";
        $pieces['fields'] .= ", imgs.post_title as thumb_title";
        $pieces['fields'] .= ", imgdata.meta_value as thumb_file";
        $pieces['join'] .= "INNER JOIN {$meta} thumbs ON ({$posts}.ID = thumbs.post_id)";
        $pieces['join'] .= " INNER JOIN {$posts} imgs ON (thumbs.meta_value = imgs.ID)";
        $pieces['join'] .= " INNER JOIN {$meta} imgdata ON (imgs.ID = imgdata.post_id)";
        $where = " AND ( thumbs.meta_key = '_thumbnail_id' AND ";
        $where .= " CAST( thumbs.meta_value AS SIGNED ) > 0 )";
        $where .= " AND ( imgdata.meta_key = '_wp_attached_file' )";
        $pieces['where'] .= $where;
        $pieces['groupby'] = " {$posts}.ID";
        return $pieces;
      }
    
      protected function parse_images( $rows ) {
        $exts = array('jpg', 'jpeg', 'gif', 'png');
        foreach ( $rows as $i => $row ) {
           $urls = wp_extract_urls( $row->post_content );
           $img = FALSE;
           while ( ! $img && ! empty($urls) ) {
             $url = array_shift($urls);
             $ext = strtolower ( pathinfo( $url, PATHINFO_EXTENSION ) );
             if ( in_array( $ext, $exts ) ) $img = $url;
           }
           $rows[$i]->thumb_link = $img ? $img : '#';
        }
      }
    }
    

    This class extends WP_Query, so accepts same things of its parent but add some filters to change the eager loading thumnail post id, thumnail post name and thumbnail file path.

    So if your query returns 50 posts, to display thumbnails you run 101 (2n+1) queries, my class you runs only 1 query.

    In addition before output the results, the class parse all the rows, and extract first image url from post content and remove the filter added.

    How to use

    Use the standard WP_Query arguments:

    $args = array(
      'post_type' => 'portfolio',
      'category_name' => 'featured',
      'posts_per_page' => 50
    );
    

    But use Thumb_Query instead of WP_Query

    $portfolio = new Thumb_Query( $args );
    

    Then loop and output:

    if ( $portfolio->have_posts() ) {
      $u = wp_upload_dir();
      global $post;
      foreach( $portfolio->posts as $post ) { 
        setup_postdata( $post );
        $f = '<div class="oneCell"><a href="%s" title="%s">';
        $f .= '<img class="lazy" alt="%s" width="189" src="%s" data-original="%s"/>';
        $f .= '</a></div>';
        $title = esc_attr( get_the_title() );
        printf( $f,
          esc_url( $post->thumb_link ),
          $title, $title,
          esc_url( get_template_directory_uri() . '/images/grey.png'  ),
          esc_url( trailingslashit($u['baseurl']) . $post->thumb_file )
        );
      }
      wp_reset_postdata();
    }
    

    Inside the loop you have access to all standard post properties, but every post has additional 4 properties:

    • $post->thumb_id the thumbnail attachment id
    • $post->thumb_title the thumbnail attachment title
    • $post->thumb_file the thumbnail file relative to uploads folder, something like '/2014/04/myfile.jpg'
    • $post->thumb_link the full url of the first image in post content or ‘#’ if no image found
  2. It is quite difficult to understand what you have done. One of my concerns is using get_the_post_thumbnail() and catch_that_image() together in the same code as you have done. I have knowledge of catch_that_image() function and have used it before. It is a nice-to-know-function that is usually used by coders to retrieve the first image to display it in the excerpt. How ever, there are better ways to get the same result without catch_that_image(). I do really think that it slows down your site as it is an outside function that runs separately from the loop, specially if you need to retrieve 56 images. catch_that_image() needs to run on every post to retrieve the image, so it is executed 56 times. Correct me if I’m wrong here.

    I don’t know what your loop looks like, but that seems to be bit of a mess. I think the best way to go here is by using WP_Query in conjuction with get_posts to construct your loop.

    In WP_Query you will only need to pass one argument to make this work, posts_per_page. You will need to set this to 1, otherwise you will get the same image multiple times.

    Next is to run the loop, and filter out all the images attached to a post using post_type and post_mime_type. WordPress saves all attachments as a post type. Not all attachments are images, so it needs to be specified that only attachments that are images should be returned. This is specified by post_mime_type.

    wp_get_attachment_image will be used to return these images at the specified size, in your code this will be the full size featured image.

    This all done and dusted, the full loop will look something like this

    <?php 
        $new_query = new WP_Query(array(
            'posts_per_page'   => 1,
        ));
    
            while ($new_query->have_posts()) : $new_query->the_post();
    
                $args = array (
                    'post_type' => 'attachment',
                    'numberposts' => 56,
                    'status' => 'publish',
                    'post_mime_type' => 'image',
                    'parent' => $post->ID
                ); 
    
                $attachments = get_posts($args);
    
                if ( $attachments ) {
                    foreach ( $attachments as $attachment ) {
                        echo '<div class="oneCell">';
                        echo '<a href="' . get_permalink( $post->ID ) . '">';
                        echo wp_get_attachment_image( $attachment->ID, 'full' );
                        echo '</a>';
                        echo '</div>';
                    }
                };
    
            endwhile;
    
    wp_reset_postdata();
    
    ?>   
    

    You just need to change the arguments to suite your exact needs, but I hope this is a basic platform that you need and can work from and that this really helps a lot with speed. Enjoy.

    Edit
    Don’t forget to reset the query using wp_reset_postdata();