November Happy Hour will be moved to Thursday December 5th.

Minesh Shah (Netcel)
Oct 12, 2023
  1908
(5 votes)

Responsive Image Rendering at the Edge

Dynamic Image Resizing

At Netcel, we recognize the significance of delivering high-performance solutions that enhance user experience and contribute to better Google rankings.

To achieve this, we prioritize serving optimized images tailored to a user's viewport size, commonly referred to as responsive images. Our approach involves advising our clients to upload the highest quality image available, while we take care of dynamically resizing and serving the appropriate version to end users.

Here's an example of what the image tag would resemble in the HTML source:

In the given example, the image file "about-intro-people-collage-4.jpg" represents the original high-quality image. The srcset attribute complements the sizes attribute by specifying various image options with their corresponding widths. Together, these attributes determine the appropriate sizes of the image for different viewport widths, display pixel density and layout.

Seeing this in Action:

Small viewport (320 pixels width)

Medium viewport (768 pixels width)

Large viewport (1200 pixels width)

As you can see with each request at different viewports the images returned were different in both storage size and width this in return meant the time taken to load was also different.

To achieve dynamic resizing and optimization of images, it is common to employ specialized tools and libraries that can handle the processing on the server side. Some popular examples of such tools are Image Resizer, Image Processor, and Six Labors ImageSharp.

Here's how the process typically works:

  1. Image Resizing and Optimization:

    These tools provide APIs or server-side libraries that can be integrated into your web application. When a request is made to an image, the server-side code utilizes these tools to resize and optimize the image on-the-fly based on the requested dimensions and optimization settings. This ensures that the image is served in the most appropriate format, size, and quality for the specific user's device and viewport.

  2. Content Delivery Network (CDN) Caching:

    Once an image is processed and served by the server, it is often beneficial to cache the optimized image in a Content Delivery Network (CDN). CDNs are distributed networks of servers located worldwide, designed to deliver content with low latency. By caching the optimized image in the CDN, subsequent requests for the same image can be served directly from the CDN servers, reducing the load on the origin server and improving response times for users across different regions.

The advantage of this approach is that the image processing and optimization tasks are performed dynamically on the server side, enabling flexibility and adaptability to different image requests. Once the image is processed and cached in the CDN, subsequent requests for the same image can be efficiently served directly from the CDN, reducing the need for repeated processing on the server.

Edge Resizing and .NET 6 Tag Helpers in Action

With the evolution to CMS12, we encountered challenges with compatibility and licensing agreements for the image optimization tools we previously used, especially in the context of .NET 6+ compatibility. We sought a solution that would alleviate the need for server-side processing and overcome these hurdles. Fortunately, Optimizely with Cloudflare are now enabling, "Image resizing at the edge for CMS and Commerce." (Beta)

This innovative feature from Optimizely allows us to leverage Cloudflare's powerful edge platform to transform images directly at the edge. It eliminates the necessity for additional tooling within our solution, removing the optimization burden from our servers entirely. Now, we can seamlessly resize, adjust quality, and even convert images to next-generation formats on demand.

The HTML tags on the front end remain unchanged. However, instead of specifying the image widths as query string parameters, we construct the image URL in a specific format to leverage Cloudflare's functionality for resizing and serving the appropriate image. For example, we might use a URL structure like "/cdn-cgi/image/width=80/siteassets/test-folder/demo.jpg" to indicate that we want Cloudflare to resize and deliver the image with a width of 80 pixels.

Model Attribution + Tag Helper

How we manage this all via code and attribute our Image Content References with the Width and Sizes attributes is quite very simple, here is an example of an Image on a Promo Block 

    [Display(
            Name = "Image",
            Description = "Image",
            GroupName = TabsGroups.Content,
            Order = 100)]
    [CultureSpecific]
    [UIHint(UIHint.Image)]
    [ImageSize(Width = 210)]
    [ImageSize(Width = 280)]
    [ImageSize(Width = 335)]
    [ImageSize(Width = 420)]
    [ImageSize(Width = 452)]
    [ImageSize(Width = 526)]
    [ImageSize(Width = 550, IsDefault = true)]
    [ImageSize(Width = 560)]
    [ImageSize(Width = 630)]
    [ImageSize(Width = 670)]
    [ImageSize(Width = 840)]
    [ImageSize(Width = 904)]
    [ImageSize(Width = 1005)]
    [ImageSize(Width = 1052)]
    [ImageSize(Width = 1100)]
    [ImageSize(Width = 1356)]
    [ImageSize(Width = 1578)]
    [ImageSize(Width = 1650)]
    [ImageSizes(Sizes = "(min-width: 1280px) 550px, (min-width: 1024px) calc(100vw - 120px) / 2, (min-width: 768px) calc(100vw - 80px) / 2, calc(100vw - 40px)")]
    [Required]
    public virtual ContentReference PromoImage { get; set; }

We have then created a simple Tag Helper to read these attributes and generate the Image Tag for us, some caveats to remember, resizing at the edge only works when routing via the CDN so not locally, also not when in the Optimizely Edit Interface. We have fallen back to not resizing in these scenarios and can be seen below. 

[HtmlTargetElement("img", Attributes = "image-for")]
public class EdgeImageTagHelper : TagHelper
{
    private readonly IUrlResolver _urlResolver;
    private readonly IContentLoaderService _contentLoader;
    private readonly IWebHostEnvironment _webHostEnvironment;
    public ModelExpression ImageFor { get; set; }
    public string CssClass { get; set; }
    public string Fit {get; set; }

    public EdgeImageTagHelper(IUrlResolver urlResolver, IContentLoaderService contentLoader, IWebHostEnvironment webHostEnvironment)
    {
        _urlResolver = urlResolver;
        _contentLoader = contentLoader;
        _webHostEnvironment = webHostEnvironment;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (ImageFor.Model is not ContentReference edgeImage) return;
        if(ContentReference.IsNullOrEmpty(edgeImage)) return;
        if(!_contentLoader.TryGet(edgeImage, out ImageFile imageFile)) return;
            
        var actualUrl = _urlResolver.GetUrl(edgeImage);
        var property = ImageFor
            .Metadata
            .ContainerType
            .GetProperties()
            .FirstOrDefault(x => x.Name == ImageFor.Metadata.PropertyName);

        if (property == null)
        {
            RenderNonResponsiveImage(output, actualUrl);
            return;
        }

        var shouldSkipEdgeResizing = _webHostEnvironment.IsDevelopment() || _contentLoader.IsContentInEditMode;
        var imageSize = property.GetCustomAttributes<ImageSizeAttribute>().ToList();
        var imageSizes = property.GetCustomAttribute<ImageSizesAttribute>();

        var defaultImageSize = imageSize.FirstOrDefault(x => x.IsDefault);
        if (defaultImageSize == null)
        {
            RenderNonResponsiveImage(output, actualUrl);
            return;
        }

        var srcSetUrls = imageSize
            .OrderBy(x => x.Width)
            .Select(s => GetEdgeUrl(actualUrl, s.Width, shouldSkipEdgeResizing)).ToList();

        SetAttribute(output, "src", GetEdgeUrl(actualUrl, defaultImageSize.Width, shouldSkipEdgeResizing, true));
        SetAttribute(output, "srcset", string.Join(",", srcSetUrls));
        if (imageSizes != null) SetAttribute(output, "sizes", imageSizes.Sizes);
        SetAttribute(output, "alt", imageFile.AlternativeText);
        SetAttribute(output, "loading", "lazy");
        SetAttribute(output, "decoding", "auto");
        SetAttribute(output, "fetchpriority", "auto");
        SetAttribute(output, "class", CssClass);

        if (imageFile is not IHasPixelSize pixelSize) return;

        if (pixelSize.Width > 0)
        {
            SetAttribute(output,"width", pixelSize.Width.ToString());
        }

        if (pixelSize.Height > 0)
        {
            SetAttribute(output, "height", pixelSize.Height.ToString());
        }
    }

    private void RenderNonResponsiveImage(TagHelperOutput output, string url)
    {
        SetAttribute(output, "src", url);
        SetAttribute(output, "class", CssClass);
    }

    private string GetEdgeUrl(string actualUrl, int width, bool isLocal, bool isDefault = false)
    {
        if (!isLocal)
        {
            return isDefault 
                ? $"/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl}" 
                : $"/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl} {width}w";
        }
        else
        {
            return isDefault
                ? $"{actualUrl}?{GetLocalImageOptions(width)}"
                : $"{actualUrl}?{GetLocalImageOptions(width)} {width}w";
        }
    }

    private string GetLocalImageOptions(int width)
    {
        var options = $"width={width}";
        if (!string.IsNullOrWhiteSpace(Fit))
        {
            options += $"&fit={Fit}";
        }

        return options;
    }

    private string GetEdgeImageOptions(int width)
    {
        var options = string.Empty;
        if (!string.IsNullOrWhiteSpace(Fit))
        {
            options = $"fit={Fit},";
        }

        options += $"width={width}";
        return options;
    }

    private void SetAttribute(TagHelperOutput output, string key, string value)
    {
        if (!string.IsNullOrWhiteSpace(value))
        {
            output.Attributes.SetAttribute(key, value);
        }
    }
}

 Then finally to utilise the Tag Helper in the Razor View we simple do the following : 

        <div class="image" @Html.EditAttributes(x=>x.PromoImage)>
            <img image-for="@Model.PromoImage" css-class="promoBlockImage" fit="cover" FlexibleAttribute="Goes Here"/>
        </div>

Tag helpers for me are easier to comprehend and manage I also have the flexibility to add any additional attributes to my HTML tag without any additional coding. 

Conclusion

The benefits of this new approach are numerous. First and foremost, the image transformation takes place at the edge, leveraging Cloudflare's global network of servers. This ensures lightning-fast delivery of optimized images with minimal latency, enhancing overall performance and user experience.

Furthermore, as the solution is provided by Optimizely and integrated into CMS12 and the Optimizely DXP, we can confidently rely on their support and compatibility with the latest versions of .NET. Say goodbye to the worries of compatibility issues and the hassle of ever-changing licensing agreements.

By leveraging "Image resizing at the edge for CMS and Commerce," we have the potential to simplify our image optimization workflow. There's no longer a need for server-side processing or the installation of additional tools. The optimization process seamlessly occurs within the Cloudflare edge platform, making our solution more scalable and efficient.

In conclusion, Optimizely's solution will empower us to overcome the challenges we faced with previous tools and server-side optimization. With image processing now taking place at the edge, we can deliver optimized images effortlessly, enhancing user experience and performance without adding complexity to our server infrastructure.

Oct 12, 2023

Comments

Ted
Ted Oct 23, 2023 01:43 PM

Great write-up! If you have clients running Adaptive Images, you can switch to Cloudflare rendering with a single line of code after adding the AdaptiveImages.Cloudflare NuGet package:

services.AddCloudflare();

I blogged some details here: https://bit.ly/cloudflare-dxp

Surjit Bharath
Surjit Bharath Dec 7, 2023 03:58 PM

Great write up. Thank you Minesh.

Please login to comment.
Latest blogs
Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog