Image Optimization with Cloudinary

Ifeoma Imoh

With websites being accessed from many devices worldwide, the attention paid to image delivery could be the difference between high customer retention and a high website bounce rate. Special attention has to be paid to the nature of the devices available and tailoring the delivered image to each device to ensure maximum satisfaction. However, it doesn’t stop there; we also need to consider the size of the image to be delivered and how this can be managed to provide a smooth user experience. This is what is referred to as image optimization.

Cloudinary has made a name for itself by not only providing a means of image storage but also providing fast CDN delivery, which helps to get resources to your users quickly. Additionally, Cloudinary automatically performs certain optimizations on transformed images and videos by default and it also provides features that enable you to optimize your media further to fit your needs.

In this article, we’ll look at how to optimize images for various screen sizes by building a simple photo album application with React. This application will allow us to upload multiple images to our Cloudinary account and then view them in a grid.

For UI components in our application, we will use antd while axios will be used for uploading our images to our Cloudinary account.

Here is a link to the demo CodeSandbox.

Setting Up the Project

Create a React application using the following command:

1npx create-react-app image_optimization_demo

Next, let’s add the project dependencies.

1npm install antd @ant-design/icons axios

Add Styling

Let's import the CSS for antd. Open your src/App.css file and edit its content to match the following:

1@import '~antd/dist/antd.css';
2
3.overlay {
4 position: fixed;
5 display: none;
6 width: 100%;
7 height: 100%;
8 top: 0;
9 left: 0;
10 right: 0;
11 bottom: 0;
12 background-color: rgba(0, 0, 0, 0.5);
13 z-index: 2;
14 cursor: pointer;
15}

We also declare the styling for a class named overlay. This will be used to blur the screen when the user is uploading images.

For the image grid, we’ll also need some custom styling. Create a new file called Grid.css in the src folder and add the following to it:

1.gallery {
2 display: grid;
3 grid-gap: 10px;
4 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
5}
6
7.gallery img {
8 width: 100%;
9}

For this tutorial, we will be sending images to Cloudinary via unsigned POST  requests. To do this, we need our account cloud name and an unsigned upload preset.

Create Helper Class for API Requests

To help with requests to the Cloudinary API, in your src folder, create a folder called util, and inside it, create a file called api.js and add the following to it:

1import axios from "axios";
2
3export const getImages = ({ successCallback }) => {
4 axios
5 .get(`https://res.cloudinary.com/YOURCLOUDNAME/image/list/image_optimization_demo.json`)
6 .then((response) => successCallback(response.data.resources));
7};
8
9export const uploadFiles = ({ selectedFiles, successCallback }) => {
10 const uploadResults = [];
11 selectedFiles.forEach((file) => {
12 uploadFile({
13 file,
14 successCallback: (response) => {
15 uploadResults.push(response);
16 if (uploadResults.length === selectedFiles.length) {
17 successCallback(uploadResults);
18 }
19 },
20 });
21 });
22};
23
24const uploadFile = ({ file, successCallback }) => {
25 const url = `https://api.cloudinary.com/v1_1/YOURCLOUDNAME/image/upload`;
26 const data = new FormData();
27 data.append("file", file);
28 data.append("upload_preset", "YOURUPLOADPRESET");
29 data.append("tags", "image_optimization_demo");
30
31 axios
32 .post(url, data, {
33 headers: {
34 "Content-Type": "multipart/form-data",
35 },
36 })
37 .then((response) => successCallback(response.data));
38};

In the getImages function, we use the Client-side asset lists feature to retrieve the list of images with the tag image_optimization_demo.

To ensure that this feature is available on your Cloudinary account, you need to enable the  Resource list option. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console and clear the Resource list item under Restricted image types. You may want to clear this option only temporarily, as needed. Alternatively, you can bypass this (and any) delivery type restriction using a signed URL.

The uploadFiles function is used to upload the selected images to Cloudinary. It iterates through the list of selected images and, using the uploadImage function, uploads the image to Cloudinary, attaching the image_optimization_demo tag to the image. This will be used to filter the images in our store and only retrieve the album images when we want to populate the grid. A callback function is also provided, which will be executed once all the images have been uploaded. This callback function also receives the result of the upload process, which can be used elsewhere in the application.

Update YOURCLOUDNAME and YOURUPLOADPRESET as specified in your Cloudinary account.

Creating the ImageUpload Component

Next, let’s create a component to upload images to Cloudinary. In the src directory, create a new folder named components.

In the src/components directory, create a new file called ImageUpload.js and add the following to it:

1import { Upload, Button, Card, Col, Row } from "antd";
2import { UploadOutlined } from "@ant-design/icons";
3import { useEffect, useState } from "react";
4import { uploadFiles } from "../util/api";
5
6const ImageUpload = ({ onCompletion }) => {
7 const [selectedFiles, setSelectedFiles] = useState([]);
8 const [showSubmitButton, setShowSubmitButton] = useState(false);
9 const [isUploading, setIsUploading] = useState(false);
10
11 useEffect(() => {
12 setShowSubmitButton(selectedFiles.length > 0);
13 }, [selectedFiles]);
14
15 const addFile = (file) => {
16 setSelectedFiles((currentSelection) => [...currentSelection, file]);
17 };
18
19 const removeFile = (file) => {
20 setSelectedFiles((currentSelection) => {
21 const newSelection = currentSelection.slice();
22 const fileIndex = currentSelection.indexOf(file);
23 newSelection.splice(fileIndex, 1);
24 return newSelection;
25 });
26 };
27
28 const beforeUpload = (file) => {
29 addFile(file);
30 return false;
31 };
32
33 const successCallback = (response) => {
34 setIsUploading(false);
35 onCompletion(response);
36 };
37
38 const handleSubmit = () => {
39 setIsUploading(true);
40 uploadFiles({ selectedFiles, successCallback });
41 };
42
43 return (
44 <Card
45 style={{
46 position: "fixed",
47 top: "50%",
48 left: "50%",
49 transform: "translate(-50%, -50%)",
50 }}
51 >
52 <Upload
53 onRemove={removeFile}
54 addFile={addFile}
55 beforeUpload={beforeUpload}
56 fileList={selectedFiles}
57 multiple={true}
58 accept="image/*"
59 >
60 {!showSubmitButton && <Button>Select files</Button>}
61 </Upload>
62 {showSubmitButton && (
63 <Row>
64 <Col offset={9} span={6}>
65 <Button
66 icon={<UploadOutlined />}
67 type="primary"
68 onClick={handleSubmit}
69 loading={isUploading}
70 >
71 Upload
72 </Button>
73 </Col>
74 </Row>
75 )}
76 </Card>
77 );
78};
79
80export default ImageUpload;

In this component, we declare three state variables - one to keep track of user file selection, another to determine whether or not the submit button should be rendered, and the last to determine whether or not files are being uploaded, and this will be used to show a loading animation on the submit button when the images are being uploaded.

We use the useEffect hook to keep track of the number of files selected by the user. Once the user has selected some files, we render the submit button to allow the user to upload the current selection. However, if the user has not selected any file, the submit button is not rendered.

The file selection in state is modified using the addFile and removeFile functions. These are used by the Upload component to update the list of files in state based on the user’s action.

The beforeUpload function is passed to the Upload component as a prop. It returns false to prevent the automatic upload of files by antd.

The handleSubmit function is used as a handler for when the Upload button is clicked. This function sets the isUploading state variable to true and calls the uploadFiles function we declared earlier to initiate the upload process.

Finally, we declare the JSX for the component, a button the user clicks to select the files they wish to upload, and a button (which is rendered conditionally depending on whether or not the user has selected any image). Using the accept prop for the Upload component, we restrict the possible selection of files to images.

Creating the ImageGrid Component

In the src/components directory, create a new file called ImageGrid.js and add the following to it:

1import "../Grid.css";
2
3const baseUrl = "https://res.cloudinary.com/YOURCLOUDNAME/image/upload";
4
5const generateSrcSet = (image) =>
6 `
7 ${baseUrl}/w_400,h_300,c_scale/v${image.version}/${image.public_id}.${image.format} 400w,
8 ${baseUrl}/w_800,h_600,c_scale/v${image.version}/${image.public_id}.${image.format} 800w,
9 ${baseUrl}/w_1200,h_900,c_scale/v${image.version}/${image.public_id}.${image.format} 1200w,
10 `;
11
12const ImageGrid = ({ images }) => {
13 return (
14 <div className="gallery">
15 {images.map((image) => (
16 <img
17 key={image.public_id}
18 src={`${baseUrl}/w_400,h_200,c_scale/v${image.version}/${image.public_id}.${image.format}`}
19 srcSet={generateSrcSet(image)}
20 sizes="(max-width: 768px) 300px,(max-width: 992px) 600px, 900px"
21 />
22 ))}
23 </div>
24 );
25};
26
27export default ImageGrid;

The ImageGrid component takes an array of Cloudinary image resources as a prop, iterates through the array, and renders each image.

To ensure that the images are optimized for screen size, in addition to providing a default image source (in the src prop), we declare two additional props - srcSet and sizes. Using these, the image source and size are set based on the screen width of the user’s device.

The string value of the srcSet prop is generated using the generateSrcSet function. The generateSrcSet function takes advantage of Cloudinary’s URL transformation feature to generate three versions of the same image on the fly.

Update YOURCLOUDNAME in the baseUrl with your cloud name.

Putting it Together

Finally, modify your src/App.js file to match the following:

1import "./App.css";
2import { UploadOutlined } from "@ant-design/icons";
3import { message, Menu } from "antd";
4import ImageGrid from "./components/ImageGrid";
5import ImageUpload from "./components/ImageUpload";
6import { useEffect, useState } from "react";
7import { getImages } from "./util/api";
8
9const items = [
10 {
11 label: "Upload Image",
12 key: "upload",
13 icon: <UploadOutlined />,
14 },
15];
16
17const App = () => {
18 const [images, setImages] = useState([]);
19 const [showImageUpload, setShowImageUpload] = useState(true);
20
21 useEffect(() => {
22 getImages({
23 successCallback: (response) => {
24 setImages(response);
25 setShowImageUpload(response.length === 0);
26 },
27 });
28 }, []);
29
30 const onCompletion = (response) => {
31 setImages(response);
32 message.success("Images uploaded successfully");
33 setShowImageUpload(false);
34 };
35
36 return (
37 <div style={{ margin: "1%" }}>
38 <div
39 className="overlay"
40 style={{
41 display: `${showImageUpload ? "block" : "none"}`,
42 }}
43 >
44 <ImageUpload onCompletion={onCompletion} />
45 </div>
46 <Menu
47 onClick={() => {
48 setShowImageUpload(true);
49 }}
50 mode="horizontal"
51 items={items}
52 style={{ marginBottom: "10px" }}
53 />
54 <ImageGrid images={images} />
55 </div>
56 );
57};
58
59export default App;

On page load, we retrieve the images from Cloudinary using the getImages function declared earlier. If the result is an empty array, then we render the ImageUpload component. If not, we pass the resources to the ImageGrid component to be rendered accordingly.

Find the complete project here on GitHub.

Conclusion

In this article, we created a basic photo album application and took advantage of the srcset and sizes HTML attribute to optimize the size of images rendered based on the user’s device screen size.

In the generation of the srcSet string, we saw one of the advantages of using Cloudinary. We were able to use the URL transformation feature to generate images of different sizes without duplicating and modifying the images for each screen size manually. However, it doesn’t stop there, as Cloudinary provides additional optimizations that make for a seamless experience.

By default, all metadata is stripped from the images being returned. This reduces the image size and the time taken to download the image. Also, by providing the images via CDN, the waiting time to retrieve the images is reduced, thus making for a quick retrieval and download process.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.