WordPress / performance

Performance plugin from the WordPress Performance Group, which is a collection of standalone performance modules.
https://wordpress.org/plugins/performance-lab/
GNU General Public License v2.0
346 stars 94 forks source link

Add direct support for Image CDN integration #15

Closed adamsilverstein closed 3 months ago

adamsilverstein commented 2 years ago
jonoalderson commented 2 years ago

This is a big need, for sure.

Performant, flexible, responsive and adaptive image markup is hard to achieve when limited by local (storage and generation) resources. Optimal approaches require large sets of srcset options, and that's prohibitively expensive to generate and store on many setups.

E.g., optimal markup for a 'hero banner' on a webpage which adapts by device (including not only viewport width, but device varying density) might look something like this:

<picture
  style="--aspect-ratio:968/500"
  class="picture-banner edge-images-picture responsive image-id-34376">
<img
  class="attachment-banner size-banner edge-images-img"
  alt=""
  decoding="async"
  height="500"
  loading="eager"
  sizes="(max-width: 968px) calc(100vw - 2.5em), 968px"
  src="https://www.example.com/path-to-image-968.jpg" width="968"
  srcset="https://www.example.com/path-to-image-1452.jpg 1452w,
      https://www.example.com/path-to-image-1936.jpg 1936w,
      https://www.example.com/path-to-image-400.jpg 400w,
      https://www.example.com/path-to-image-500.jpg 500w,
          https://www.example.com/path-to-image-600.jpg 600w,
      https://www.example.com/path-to-image-700.jpg 700w,
      https://www.example.com/path-to-image-800.jpg 800w,
      https://www.example.com/path-to-image-900.jpg 900w,
      https://www.example.com/path-to-image-1000.jpg 1000w,
      https://www.example.com/path-to-image-1200.jpg 1200w,
      https://www.example.com/path-to-image-1400.jpg 1400w,
      https://www.example.com/path-to-image-1600.jpg 1600w,
      https://www.example.com/path-to-image-1800.jpg 1800w,
      https://www.example.com/path-to-image-968.jpg 968w,
      https://www.example.com/path-to-image-484.jpg 484w">
</picture>

That's a huge number of image variations to handle locally.

That number increases with each additional format or context supported; which extends not only to image formats, but also might include browser preferences (such as dark mode, or animation preferences).

At the moment, solutions to this kind of challenge rely heavily upon offloading the generation and storage of images. Without a standardised API to do this, they do it via 'brute force', and continually have to reinvent the wheel. A generalised API for requesting generative and external images would be a great feature.

adamsilverstein commented 2 years ago

also possibly related: https://github.com/WordPress/performance/issues/8

Without a standardised API to do this, they do it via 'brute force', and continually have to reinvent the wheel.

This is exactly the motivation for this issue. Would love to hear more ideas about how we can design such an API in core.

dainemawer commented 2 years ago

Agreed, I think this is super important! +1 from me! To add to this, depending on what CDN provider is chosen to run with - we should preconnect to the origin by placing: <link rel="preconnect" href="https://cdn.cloudinary.com"> in the head as part of the API. This should optimise the round trip requests that are needed

khoipro commented 2 years ago

We can add a new field on Settings / Media to enable and set CDN domain. Some plugins support CDN, but not all will replace it. I strong suggest take a code from my plugin: https://vi.wordpress.org/plugins/ct-optimization

adamsilverstein commented 2 years ago

Hey @khoipro thanks for sharing, can you add a link to your code where you add a CDN field in your code and maybe explain how you use it?

khoipro commented 2 years ago

Sorry for the late response. Here is my example code.

In wp-config.php

define( 'CDN_URL', 'https://cdn.domain.com' );

In theme's functions.php

if (defined('CDN_URL') ) {
    add_filter('wp_get_attachment_url', 'npn_cdn_attachments_urls', 10, 2);
    add_filter('wp_get_attachment_image_src', 'npn_cdn_attachment_image_src', 10, 2);
    add_filter('wp_calculate_image_srcset', 'npn_cdn_calculate_image_srcset');
    add_filter('wp_get_attachment_image_srcset', 'npn_cdn_attachment_srcset_filter');
}

function npn_cdn_attachments_urls($url, $post_id)
{
  return str_replace(site_url( '/wp-content/uploads' ), CDN_URL, $url);
}

function npn_cdn_calculate_image_srcset($sources)
{
    foreach ($sources as &$source) {
      $source['url'] = str_replace( site_url( '/wp-content/uploads'), CDN_URL, $source['url']);
    }

    return $sources;
}

function npn_cdn_attachment_srcset_filter($attr)
{
    if (!empty($attr['srcset'])) {
        $attr_srcset = $attr['srcset'];
        $attr['srcset'] = str_replace( site_url( '/wp-content/uploads'), CDN_URL, $attr_srcset);
    }

    return $attr;
}

function npn_cdn_attachment_image_src($image, $attachment_id) {
    $image[0] = str_replace(site_url( '/wp-content/uploads' ), CDN_URL, $image[0]);

    return $image;
}
khoipro commented 2 years ago

It works perfectly with Object Storage with/without CDN from my above configuration. In case a CDN contains full path, like cdn.xxx.com/wp-content/uploads/, we could look to use this code: https://github.com/codetot-web/ct-optimization/blob/master/includes/class-codetot-optimization-process.php#L344

Rod-Gomes commented 1 year ago

Hello @khoipro ,

Are you sure your code above works as expected on various themes? I'm building on it for another project, but I noticed that there are some issues with some themes, related to wp_get_attachment_url and wp_get_attachment_image_src. In some themes, the image link disappears.

My code:

function imageurl_replace($matches) {
    $url = parse_url(get_site_url());
    $host = str_replace("www.", "", $url['host']);
    $hostregex = str_replace("/", "\/", $host);
    $hostregex = str_replace(".", "\.", $hostregex);

    // Check if image is compatible and if it's the same domain as the website
    if (!preg_match("/(https?:)?\/\/(www.)?".$hostregex."\/((?![\"']).)*\.(jpe?g|gif|png|webp)($|\?.*)/i", $matches)) {
        return $matches;
    }

    // Replaces the url with cdn url
    $cdn = 'mycdn.com/';

    $dslash_pos = strpos($matches, '//') + 2;
    $src_pre  = substr($matches, 0, $dslash_pos); // http:// or https://
    $src_post = substr($matches, $dslash_pos); // The rest after http:// or https://

    return $src_pre . $cdn . $src_post;
}

add_filter( 'wp_get_attachment_url', function($url) {
    $url = imageurl_replace($url);
    return $url;
}, 10, 2);

add_filter('wp_get_attachment_image_src', function($image) {
    if (is_array($image) && !empty($image[0])) {
        $image[0] = imageurl_replace($image[0]);
    }
    return $image;
}, 10, 2);

add_filter( 'wp_calculate_image_srcset', function($sources) {
    if ((bool) $sources) {
        foreach ($sources as $width => $data) {
            $sources[ $width ]['url'] = imageurl_replace($data['url']);
        }
    }
    return $sources;
});

add_filter( 'wp_get_attachment_image_srcset', function($attr) {
    if (!empty($attr['srcset'])) {
        $attr_srcset = $attr['srcset'];
        $attr['srcset'] = imageurl_replace($attr_srcset);
    }
    return $attr;
});
adamsilverstein commented 3 months ago

Thanks all for the feedback here. Thanks for sharing your code @khoipro. The idea would be to add a simpler surface in core so you could accomplish the same thing with a single filter, or a single function call to register your handler, maybe with a specification by mime type.

I plan to follow up on this with a core trac ticket, and closing this ticket for now.