Wordpress - How to generate thumbnails when needed only?

Take a look at Otto's Dynamic Image Resizer plugin

This plugin changes the way WordPress creates images to make it generate the images only when they are actually used somewhere, on the fly. Images created thusly will be saved in the normal upload directories, for later fast sending by the webserver. The result is that space is saved (since images are only created when needed), and uploading images is much faster (since it's not generating the images on upload anymore).

Put this in your theme functions file. It will stop Wordpress from creating anything but the 3 default sizes when uploading.

When an image is then requested in a particular size, which is not yet generated, it will be created only that once.

        add_filter('image_downsize', 'ml_media_downsize', 10, 3);
        function ml_media_downsize($out, $id, $size) {
            // If image size exists let WP serve it like normally
            $imagedata = wp_get_attachment_metadata($id);
            if (is_array($imagedata) && isset($imagedata['sizes'][$size]))
                return false;

            // Check that the requested size exists, or abort
            global $_wp_additional_image_sizes;
            if (!isset($_wp_additional_image_sizes[$size]))
                return false;

            // Make the new thumb
            if (!$resized = image_make_intermediate_size(
                return false;

            // Save image meta, or WP can't see that the thumb exists now
            $imagedata['sizes'][$size] = $resized;
            wp_update_attachment_metadata($id, $imagedata);

            // Return the array for displaying the resized image
            $att_url = wp_get_attachment_url($id);
            return array(dirname($att_url) . '/' . $resized['file'], $resized['width'], $resized['height'], true);

        add_filter('intermediate_image_sizes_advanced', 'ml_media_prevent_resize_on_upload');
        function ml_media_prevent_resize_on_upload($sizes) {
            // Removing these defaults might cause problems, so we don't
            return array(
                'thumbnail' => $sizes['thumbnail'],
                'medium' => $sizes['medium'],
                'large' => $sizes['large']

Unfortunately @Patrick's answer breaks the srcset functions introduced in WP 4.4. Fortunately, we just need to add two additional functions!

First, we need to temporarily re-introduce all of the registered thumbnail sizes to the image metadata so they can be considered:

function bi_wp_calculate_image_srcset_meta($image_meta, $size_array, $image_src, $attachment_id){
    //all registered sizes
    global $_wp_additional_image_sizes;

    //some source file specs we'll use a lot
    $src_path = get_attached_file($attachment_id);
    $src_info = pathinfo($src_path);
    $src_root = trailingslashit($src_info['dirname']);
    $src_ext = $src_info['extension'];
    $src_mime = wp_check_filetype($src_path);
    $src_mime = $src_mime['type'];
    $src_base = wp_basename($src_path, ".$src_ext");

    //find what's missing
    foreach($_wp_additional_image_sizes AS $k=>$v)
            //first, let's find out how things would play out dimensionally
            $new_size = image_resize_dimensions($image_meta['width'], $image_meta['height'], $v['width'], $v['height'], $v['crop']);
            $new_w = (int) $new_size[4];
            $new_h = (int) $new_size[5];

            //bad values
            if(!$new_h || !$new_w)

            //generate a filename the same way WP_Image_Editor would
            $new_f = wp_basename("{$src_root}{$src_base}-{$new_w}x{$new_h}." . strtolower($src_ext));

            //finally, add it!
            $image_meta['sizes'][$k] = array(
                'file'      => $new_f,
                'width'     => $new_w,
                'height'    => $new_h,
                'mime-type' => $src_mime

    return $image_meta;
add_filter('wp_calculate_image_srcset_meta', 'bi_wp_calculate_image_srcset_meta', 10, 4);

Then we need to run through the matches and generate any missing thumbnails:

function bi_wp_calculate_image_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id){

    //get some source info
    $src_path = get_attached_file($attachment_id);
    $src_root = trailingslashit(pathinfo($src_path, PATHINFO_DIRNAME));

    //the actual image metadata (which might be altered here)
    $src_meta = wp_get_attachment_metadata($attachment_id);

    //an array of possible sizes to search through
    $sizes = $image_meta['sizes'];

    $new = false;

    //loop through sources
    foreach($sources AS $k=>$v)
        $name = wp_basename($v['url']);
            //find the corresponding size
            foreach($sizes AS $k2=>$v2)
                //we have a match!
                if($v2['file'] === $name)
                    //make it
                    if(!$resized = image_make_intermediate_size(
                        //remove from sources on failure
                        //add the new thumb to the true meta
                        $new = true;
                        $src_meta['sizes'][$k2] = $resized;

                    //remove from the sizes array so we have
                    //less to search next time
            }//each size
        }//each 404
    }//each source

    //if we generated something, update the attachment meta
        wp_update_attachment_metadata($attachment_id, $src_meta);

    return $sources;
add_filter('wp_calculate_image_srcset', 'bi_wp_calculate_image_srcset', 10, 5);