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';23.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}67.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";23export const getImages = ({ successCallback }) => {4 axios5 .get(`https://res.cloudinary.com/YOURCLOUDNAME/image/list/image_optimization_demo.json`)6 .then((response) => successCallback(response.data.resources));7};89export 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};2324const 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");3031 axios32 .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";56const ImageUpload = ({ onCompletion }) => {7 const [selectedFiles, setSelectedFiles] = useState([]);8 const [showSubmitButton, setShowSubmitButton] = useState(false);9 const [isUploading, setIsUploading] = useState(false);1011 useEffect(() => {12 setShowSubmitButton(selectedFiles.length > 0);13 }, [selectedFiles]);1415 const addFile = (file) => {16 setSelectedFiles((currentSelection) => [...currentSelection, file]);17 };1819 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 };2728 const beforeUpload = (file) => {29 addFile(file);30 return false;31 };3233 const successCallback = (response) => {34 setIsUploading(false);35 onCompletion(response);36 };3738 const handleSubmit = () => {39 setIsUploading(true);40 uploadFiles({ selectedFiles, successCallback });41 };4243 return (44 <Card45 style={{46 position: "fixed",47 top: "50%",48 left: "50%",49 transform: "translate(-50%, -50%)",50 }}51 >52 <Upload53 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 <Button66 icon={<UploadOutlined />}67 type="primary"68 onClick={handleSubmit}69 loading={isUploading}70 >71 Upload72 </Button>73 </Col>74 </Row>75 )}76 </Card>77 );78};7980export 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";23const baseUrl = "https://res.cloudinary.com/YOURCLOUDNAME/image/upload";45const 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 `;1112const ImageGrid = ({ images }) => {13 return (14 <div className="gallery">15 {images.map((image) => (16 <img17 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};2627export 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";89const items = [10 {11 label: "Upload Image",12 key: "upload",13 icon: <UploadOutlined />,14 },15];1617const App = () => {18 const [images, setImages] = useState([]);19 const [showImageUpload, setShowImageUpload] = useState(true);2021 useEffect(() => {22 getImages({23 successCallback: (response) => {24 setImages(response);25 setShowImageUpload(response.length === 0);26 },27 });28 }, []);2930 const onCompletion = (response) => {31 setImages(response);32 message.success("Images uploaded successfully");33 setShowImageUpload(false);34 };3536 return (37 <div style={{ margin: "1%" }}>38 <div39 className="overlay"40 style={{41 display: `${showImageUpload ? "block" : "none"}`,42 }}43 >44 <ImageUpload onCompletion={onCompletion} />45 </div>46 <Menu47 onClick={() => {48 setShowImageUpload(true);49 }}50 mode="horizontal"51 items={items}52 style={{ marginBottom: "10px" }}53 />54 <ImageGrid images={images} />55 </div>56 );57};5859export 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: