Images are an essential medium for user engagement. A study by Skyward observed that using meaningful images and infographics increases page views by 94% compared to pages without any images. Clearly, images are an asset that developers can't afford to ignore.

However, images are often the main culprit behind increased page load times and high bandwidth usage. A slow website can potentially lead to losses in business and user engagement, and ensuring images retain a high visual quality while keeping their data footprint minimal is an ongoing challenge. There’s also the matter of responsive design, to make sure images adapt to various user screen sizes and resolutions seamlessly.

Optimized images have several benefits:

  • Faster Load Times: Smaller image sizes mean quicker downloads, meaning reduced page load times.
  • Lower Bandwidth Usage: Optimized images consume less bandwidth on the user side, benefiting those on limited data plans or slower networks. This is crucial In a mobile-first era, where the majority of internet access occurs via smartphones.
  • Improved User Engagement: Users are more likely to engage with content and click on internal links when they don't have to wait for images to load. This has a direct effect on bounce rates as a fast, snappy user experience means users are more inclined to stay and explore.
  • Improved SEO: Search engines prioritize faster-loading pages so image optimization can contribute positively to your site's search rankings.

In this post, we’ll delve into React image optimization, exploring a variety of techniques and tools that can help you optimize your images, dramatically improving page load times and user engagement, and reducing bounce rates. You'll also learn how to leverage ImageKit for all your image optimization needs and stay ahead of the curve.

So, without further ado, let's dive in.

Image Optimization Techniques in React

The first thing to know about optimizing images for React apps is that you can’t take a one-size-fits-all approach. Any effective strategy has to be a combined approach with various techniques and tools, all tailored to meet the specific needs of a React application.

💡
React frameworks like Next.js provide out-of-the-box tools like the next/image component that automatically optimizes images on-demand, as users request them, automatically resizing, lazy loading, and avoiding Cumulative Layout Shift while serving images in modern formats like WebP (when the browser supports it). But you don’t get such features in vanilla React, itself, and this makes image optimization more complicated.

First, let’s discuss a technique that focuses on reducing the file size of images before rendering, that is before they are bundled with the web application. The goal is to minimize the initial load time of the page.

Build-time Optimization using image-webpack-loader

Webpack is a static module builder for JavaScript applications. A module is a self-contained unit of code that can be independently loaded, reused, and even shared between different parts of your application.

One such Webpack module that allows you to automate image optimization in a React application during the build process is the image-webpack-loader package. By using this module in your Webpack configuration, you can make image optimization a part of your build pipeline itself. However, one of the significant drawbacks of using this method is that you’ll need to have the images stored as a part of the codebase, which is not an ideal solution. In most of the real–world applications, the images usually are not stored in the codebase.

This loader enables you to compress images in various formats, such as JPEG, PNG, GIF, SVG, and even WebP, directly within your Webpack configuration, minifying these assets for you before they are bundled with the actual webpage.

💡
The prerequisite to working with the image-webpack-loader is that your React application must be initialized with Webpack and Babel. This article will not walk you through the process, but you can follow this guide for initializing a React application with the two mentioned tools.

To compress images using this loader, first, install the package using NPM or Yarn:

# Using npm
npm install image-webpack-loader --save-dev

# Using yarn
yarn add image-webpack-loader --dev

After installation, add the image-webpack-loader to the webpack.config.js file under the rules array. The rules array under module object should look like this:

  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.(gif|png|jpe?g|svg)$/i,
        use: [
          "file-loader",
          {
            loader: "image-webpack-loader",
            options: {
              mozjpeg: {
                progressive: true,
              },
              // optipng.enabled: false will disable optipng
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              },
              gifsicle: {
                interlaced: false,
              },
            },
          },
        ],
      },
    ],
  },

In the above example, file-loader is used for resolving the image paths, and image-webpack-loader is used for compressing the images. The options key provides various image optimization libraries such as MozJPEG, OptiPNG, PNGQuant, GIFsicle, and WebP to apply compression techniques to your images. These libraries can reduce the file size of images without significantly compromising their quality, each customizable, giving you fine-grained control over how each format should be compressed to reduce their size while maintaining visual clarity.

Let's break down the options used in the example:

  1. mozjpeg: This option is for compressing JPEG images. The progressive key enables progressive rendering. Progressive JPEGs load in multiple passes, showing an increasingly detailed image with each pass. The quality is a number between 0 and 100. The lower the number, the smaller the file size, but at the cost of image quality.
  2. optipng: It compresses PNG images. The enabled boolean value enables or disables the plugin.
  3. gifsicle: Used for compressing GIFs. The interlaced boolean value allows the image to render, making it appear to load faster progressively.

If you run your application with some images in it, you’ll be able to see the difference between the sizes. For example, the image below demonstrates the file size after rendering. The original file size of the image is 2.3MB, whereas the image-webpack-loader plugin compressed it to 1.4 MB.

Image Optimization using image-webpack-loader.

The code used in the above example can be found in this GitHub repository.

Advantages of using image-webpack-loader

  • Automated Process: Once set up, the module automates the image optimization process as part of your build pipeline itself. It ensures that all images in your project are consistently optimized without manual intervention, saving you time and effort.
  • Supports Multiple Formats: This approach supports multiple formats, such as JPEG, PNG, GIF, SVG, etc. This flexibility ensures that images in different formats can be optimized to reduce file sizes while maintaining their specific characteristics.
  • Fine-grained Control: Allows you to customize compression algorithms, control the compression ratios and quality, and format conversions for each image type, tailoring optimization to specific requirements.

Drawbacks of using image-webpack-loader

  • Images Must Reside in Codebase: A significant limitation of using image-webpack-loader is the necessity for all images to be part of the codebase. In many real-world applications, images are often fetched from external sources or content management systems, making this approach less flexible and potentially unsuitable for projects that rely on dynamic or externally hosted images.
  • Configuration Complexity: Configuring the Webpack module method for image optimization can become complex, and require a thorough understanding of Webpack configuration, which could be challenging for some developers. In fact, installing this module itself is a cumbersome process, requiring a lot of additional packages to be installed to support the Webpack configuration. There also may be potential issues when using specific Node.js versions. It might be helpful to check this module’s documentation, here.
  • Increased Build Time: Image optimization as part of the build process can increase the time it takes to build your project, especially if you have a large number of images.
  • Quality Trade-offs: While image-webpack-loader provides granular control over its compression settings, its default settings might be aggressive for some use cases, potentially leading to a noticeable loss in image quality. Developers might need to invest time in fine-tuning these settings to achieve the desired balance between file size reduction and visual aesthetics.

Optimize Image Uploads using browser-image-compression

If you’re allowing client-side image uploads, Browser Image Compression is a package that allows you to compress images on the browser, before they are sent to a server. The module can be used to compress JPEG, PNG, WebP, and BMP images, works with React (or any other framework), and comes with built-in support for multi-threading (web workers) for non-blocking compression.

Let’s take a look at a React component that compresses images using the Browser Image Compression package:

import imageCompression from 'browser-image-compression';
import testImg from '/src/assets/img.jpg';
import { useEffect, useState } from 'react';
// fetches an image from a URL and returns it as a Blob object.
const fetchImageAsBlob = async (imageUrl) => {
  const response = await fetch(imageUrl);
  const blob = await response.blob();
  return blob;
};

// takes a Blob object, compresses it, and returns the compressed Blob object
const compressBlobImage = async (blob) => {
  const options = {
    maxSizeMB: 1, // set a max file size in megabytes after compression.
    maxWidthOrHeight: 1920,// set a max dimension (either width or height) for the output image
    useWebWorker: true,// use a web worker for the compression task, offloading the main thread.
  };
  const compressedFile = await imageCompression(blob, options);
  return compressedFile;
};

const ImageComponent = () => {
  const [compressedImage, setCompressedImage] = useState(null);

  useEffect(() => {
    let blobUrl = null;

    const processImage = async () => {
      try {
        // Step 1 : Fetch the Blob object of the original image
        const blob = await fetchImageAsBlob(testImg);
        // Step 2 : Compress the Blob object
        const compressedFile = await compressBlobImage(blob);
        // Step 3 : Create a Blob URL for the compressed image
        blobUrl = URL.createObjectURL(compressedFile);
        // Step 4 : Update the state with the compressed image's Blob URL.
        setCompressedImage(blobUrl);
      } catch (error) {
        console.log('Image processing failed:', error);
      }
    };

    processImage();
   // ensures that the Blob URL is revoked when the component unmounts to free up the resources.
    return () => {
      if (blobUrl) {
        URL.revokeObjectURL(blobUrl);
      }
    };
  }, []);

  return (
    <div>
      {compressedImage ? (
        <img src={compressedImage} height={300} width={500} alt='Compressed' />
      ) : (
        'Loading...'
      )}
    </div>
  );
};

export default ImageComponent;
💡
Please note the above snippet is not intended to be used in production. Rather it is for demonstration only. The primary purpose of browser-image-compression is to compress the image before uploading it to the server for saving bandwidth.

The ImageComponent React component fetches, compresses, and displays the image. The compressedImage state holds the Blob’s URL of the compressed image.

Image optimization using browser-image-compression.

In the image above, the first image is the uncompressed one. The original size of the image is around 4MB, whereas the image below is compressed with the browser-image-compression package, optimizing the size to around 569KB only. You can find the code for the working example here.

You can use this package to compress the images on the client side before uploading them to the server for saving storage.

Advantages of using browser-image-compression

  • Immediate Feedback: Compression happens on the browser (i.e. client side), providing instant feedback to the user.
  • Customizable: The browser-image-compression library offers multiple options to fine-tune the compression based on the application's requirements.
  • Offloads Main Thread: The built-in support for web workers (useWebWorker: true) prevents the UI from becoming unresponsive during the compression process.

Drawbacks of using browser-image-compression

  • Processing Time: Compression takes time and CPU resources on the client side, which might be noticeable on older or less powerful devices.
  • Quality Loss: Compression usually comes with some level of quality loss, which might not be suitable for all types of images or applications.
  • Dependency: This relies on an external package (browser-image-compression), which means you have to keep it up to date and be aware of any vulnerabilities or issues that arise in the library.

Image Optimization using Microservices

The @mts-pjsc/image-optimize is an NPM package designed to optimize images server-side to enhance website speed. However, you must deploy the image-optimize microservice on a server to unlock its potential. This microservice offers capabilities such as image resizing and compression.

Deployment is made convenient through Docker containers, with the official documentation providing the necessary guidance.

After successfully deploying the microservice, you’ll have to ensure it's available via the /optimizer endpoint. With that in place, you can begin to integrate the @mts-pjsc/image-optimize package into your application. The process involves substituting the standard img tags with the Image tag from the package:

import { Image } from "@mts-pjsc/image-optimize";

function MyComponent() {
  return (
    <Image
      alt="Example"
      src="/image-path.png"
    />
  )
}

export default MyComponent;

Advantages of using a dedicated microservice

  • Feature-Rich Microservice: The associated microservice offers several image optimization capabilities, including resizing and compression.
  • Docker Deployment: The microservice can be deployed using Docker containers, making the deployment process streamlined and consistent across different environments.
  • Simplified Integration: Once set up, you can easily optimize images in React applications by replacing standard img tags with the Image tag from the package.

Disadvantages of using a dedicated microservice

  • Dependency on External Service: The effectiveness of the NPM package relies on the separate deployment of the image-optimize microservice. This introduces an external point of failure.
  • Infrastructure Overhead: Deploying and maintaining the microservice, especially at scale, might introduce additional costs and complexities (especially since Docker know-how becomes a requirement).
  • Potential Latency: Making requests to an external microservice might introduce latency, especially if the microservice and the web application are not in the same region or network.

Optimizing Images as Users interact with the website

The following techniques are more concerned with improving the user experience and page load performance as the user interacts with the website. Let’s have a look at some of these.

1. Lazy Loading:

Lazy loading defers the loading of non-critical resources at page load time. Instead, these resources are loaded when they're needed, resulting in a faster initial page load time. In the context of images, it means that images will only be loaded when they enter or are about to enter the viewport.

Starting with React 16.6.0, the lazy() function has been introduced, allowing the rendering of a dynamic import as a regular component.

const LazyImage = React.lazy(() => import('./ImageComponent'));
   
   function MyComponent() {
     return (
       <div>
         <React.Suspense fallback={<div>Loading...</div>}>
           <LazyImage />
         </React.Suspense>
       </div>
     );
   }

You can also use the traditional HTML5 `lazy` attribute for images:

<img src="image.jpg" loading="lazy" alt="example" />

2. Using srcSet for Responsive Images

It’s essential that your images look crisp on all devices. With the srcset attribute, you can define multiple image sources that are conditionally loaded based on the device's screen size.

const ExampleComponent = () => (
  <img
    srcSet="small.jpg 500w, medium.jpg 1000w, large.jpg 2000w"
    src="medium.jpg"
    alt="example"
  />
);


export default ExampleComponent;

However, to utilize srcSet, you must create and provide various size variations of your image beforehand, ensuring they're optimized for different screen dimensions. This increases the time required to serve an image responsively.

3. Using Advanced Formats

WebP and AVIF are modern image formats that provide superior lossless and lossy compression for images on the web. WebP images are, on average, 30% smaller in size compared to JPEGs. Again, to use these modern formats in your applications, it is required that you create them beforehand for each format.

For example, to support AVIF, and WebP, you’ll need to create both variations of the image, store all the variations in the application, and then serve them.

If you are curious about other advanced image formats, read this blog on all the possible best image formats for the web.

Here’s an example of using these advanced image formats in React:

const ExampleComponent = () => {
  return (
    <picture>
      <source srcSet="image.avif" type="image/avif" />
      <source srcSet="image.webp" type="image/webp" />
      <img src="image.jpg" alt="example" />
    </picture>
  );
};


export default ExampleComponent;

In the above code, the picture element is used to provide multiple image sources to the browser. This element acts as a container for different image formats, allowing browsers to pick the best-suited image based on their capabilities.

The source element here offers a choice between two modern image formats:

  • AVIF for high compression and quality.
  • WEBP for efficient compression.

Finally, the img element acts as a fallback. If the browser doesn't support AVIF or WEBP, it defaults to the JPG image. You can refer to this to know more about the picture element.

The Inherent Drawbacks of Common Optimization Techniques

Even though the techniques we’ve covered are good solutions for image optimization on their own, they might not be ideal in the long term. Here's a look at the inherent trade-offs of these techniques:

  1. Assets must be created and maintained manually: All these methods require you to manually create, manage, and host multiple image variations tailored for different devices/screen resolutions. They can’t dynamically adapt or resize images on the fly for you, or generate fallbacks or variant assets.

    This not only increases the asset management overhead, but also requires careful consideration of how and when each variant should be served to provide an optimal user experience. Proper testing across various devices and resolutions becomes crucial to ensure consistent visual representation.
  2. Loss of Quality or Detail Due to Compression: Aggressive image optimization settings can lead to a loss of image quality, or finer details. This could be problematic for specific use cases like e-commerce which drives engagement through high-quality product images.

    Even with these techniques, you’ll still need to find an acceptable middle ground: compression that offers significant file size reduction without noticeably affecting the user experience, and even then, this may require ongoing adjustments.
  3. Increased Development Complexity: All of these strategies can introduce complexity into your development workflow. Configuring and maintaining Webpack modules, libraries, or microservices for image optimization, and implementing responsive image solutions, require additional development time and effort.

    While the payoff is a more performant website, you need to keep in mind that doing all of this yourself will be a trade-off between development time vs. user experience.
  4. Resource Intensive: Webpack modules can increase the time it takes to build your project, and in-browser image optimization libraries can consume client-side resources. Both are prominent issues, especially when processing a large number of images.

When diving into image optimization, it's crucial to understand the technical limitations and tradeoffs of these techniques. None of these provide an optimal, balanced solution.

That’s why a third-party image optimization service like ImageKit, that provides automation, flexibility, and the best possible optimization without degrading the quality of the image, is key to offering a great user experience without devoting hours of dev time.

Image Optimization with ImageKit in React

While React offers a powerful foundation for building dynamic web applications, ImageKit mitigates the pain of manually handling and optimizing your assets, giving you a superior experience whether you’re working with images or video, and covering your entire asset management pipeline from storage to delivery. ImageKit provides digital storage and management, real-time image and video optimization, transformations, and edge delivery using a global CDN out of the box.

Let’s discuss a few critical features ImageKit provides that can help you optimize images for your React apps.

  • Automatic Format Conversion: ImageKit’s automatic format conversion automatically delivers the best image format to the end-user depending on various factors like device capabilities, browser support, image content, image quality, and more.

This means you can have a single high-quality source image (stored on ImageKit’s Media Library, or your existing storage solution), and from this image,  automatically serve modern formats like AVIF to devices/browsers that support it (which send image/avif in the Accept request header), WebP if the browser or device is set up for this, or JPEG to others without any extra configuration or URL change. All you need is a simple dashboard setting.

Just turn on the Use best format for Image Delivery from your ImageKit dashboard, and ImageKit will handle the rest. Activating this setting alone will allow you to serve optimized images without doing anything in your application frontend code.

  • Real-time Image Transformation and Manipulation: With ImageKit, you don’t need to manually create multiple variations of an image beforehand. Using a developer-friendly URL-based API, it's possible to adjust dimensions, apply filters, and conduct a host of other transformations. This on-the-fly customization ensures that a single source image can be served in different layouts or devices, and redesigns and layout changes are easier.

For instance, if you want to resize an image to 300x300 pixels and convert it to grayscale, add transformations as URL params:

<img src="https://ik.imagekit.io/demo/tr:w-300,h-300,e-grayscale/default-image.jpg" alt="Sample Image" />

The w-300,h-300 resizes the image to 300x300 pixels, and the e-grayscale converts your image to grayscale.

Smart Cropping and Resizing: Generic center cropping often results in key subjects being cut off from images, especially in responsive designs. ImageKit's intelligent cropping functions, where ImageKit automatically determines the most important part of the image or even face detection, ensure that the heart of the image remains intact. So, regardless of device or viewport size, users always see the image's essence. To enable the smart cropping, you can either pass the fo-auto parameter (for smart cropping), or the fo-face parameter (for cropping with face detection). Here’s an example of cropping an image based on the face:

<img src="https://ik.imagekit.io/demo/img/tr:w-450,h-450,fo-face/girl.jpeg" alt="Face of a girl" />

Adding an extra fo-face parameter did all the heavy lifting for you.

  • CDN Delivery and Caching: ImageKit leverages global Content Delivery Networks (CDN), so that images load lightning-fast by serving them from the nearest possible location to the user. Beyond just initial delivery, the CDN that serves the images caches them at the edge locations and reduces the wait times for subsequent visits. You get the power of global CDN delivery (ImageKit comes with AWS CloudFront out of the box) and caching without any extra setup. You can even use a custom CDN if you already have one.
  • Robust SDKs for Asset Management: Beyond just optimization, transformation, and delivery, ImageKit also provides server and client-side SDKs for image uploads, management, and storage in the cloud. It acts as a unified platform for all your needs, making your entire digital asset pipeline as streamlined as possible.

Incorporating ImageKit into your React applications takes the heavy lifting of image optimization entirely off your shoulders, removing bloat from your frontend code. With ImageKit’s comprehensive features, you ensure that your applications not only function optimally, but also deliver a great experience for both your developers and your users.

Conclusion

The aim of the article was to help you through the process of optimizing images in React. You have explored the possible solutions using Webpack, microservices, and other packages. All strategies mentioned here require extra effort to serve the best possible image without sacrificing quality or load times or consuming extra bandwidth/storage.

When optimizing images in React, ImageKit can be a great solution as you don’t have to worry about dependency management, vulnerabilities, or the extra overhead of setting up. You use the regular img tags in your application and use the ImageKit public URL for serving images, and you are good to go. Set and forget.

ImageKit also provides you with a free plan, so you can only upgrade when satisfied with the service. Also, ImageKit provides a React SDK to help you quickly get up and running for all your image optimization needs. The getting started guide tailored for all level of developers make it extremely easy to get started.

Sign up today to explore ImageKit.