Several techniques have been adapted over time to optimize images, one of which is image compression.
In this article, we will build an app that uses an image compression module to compress images and display the resulting size. As bonus steps, we will add the options to communicate with a serverless function to upload the compressed file to Cloudinary or download the image to your computer.
To proceed, you are expected to have at least a basic knowledge of React.js, and here’s a link to the demo on CodeSandbox.
Run the following command in your terminal to create a Next.js application in a folder called image-compressor
.
npx create-next-app image-compressor
Next, we install the necessary dependencies.
npm install --save browser-image-compression cloudinary axios vercel
The browser-image-compression module will be used to handle image compression. The Cloudinary node SDK will be used in our serverless function to upload images to Cloudinary, and axios will be our HTTP client.
At the root of our project, let’s create a folder called components
and add two files to it. Run the following in your terminal:
mkdir components
cd components
touch fileinput.js imagepreviewer.js
Code language: CSS (css)
Add the following to your fileInput.js
file:
import styles from "../styles/Home.module.css";
export default function FileInput({ handleOnChange }) {
return (
<div className={styles["input-group"]}>
<label htmlFor="input-file">Select Image</label>
<input
type="file"
id="input-file"
accept="image/*"
onChange={handleOnChange}
style={{ display: "none" }}
/>
</div>
);
}
Code language: JavaScript (javascript)
This component renders a label and an input field that accepts only image files.
Let’s create a file that’ll hold our utility functions. At the root of your project, create a file called helpers.js
. Now, we’ll define a helper method that will help us compress our image. Add the following to the helper.js
file:
import imageCompression from "browser-image-compression";
const defaultOptions = {
maxSizeMB: 1,
};
export function compressFile(imageFile, options = defaultOptions) {
return imageCompression(imageFile, options);
}
Code language: JavaScript (javascript)
This method expects an image file object and an optional options object. Internally, it uses the image compression module to return a promise that resolves to the compressed version of the image file based on the options passed. The image compression module accepts a wide range of options that allows you to customize how the compression is performed. We also set the maximum size of the compressed file to 1 megabyte in the default settings object.
Add the following to your index.js
file:
import { useState } from "react";
import FileInput from "../components/FileInput";
import ImageOutput from "../components/ImageOutput";
import ImagePreviewer from "../components/imagePreviewer";
import { compressFile, download, readFileAsBase64 } from "../helpers/helpers";
import axios from "axios";
import styles from "../styles/Home.module.css";
export default function App() {
const [selectedImage, setSelectedImage] = useState(null);
const [compressedImage, setCompressedImage] = useState();
const [isCompressing, setIsCompressing] = useState(false);
const handleOnChange = (event) => {
setSelectedImage(event.target.files[0]);
};
const handleCompressFile = async () => {
if (selectedImage) {
try {
const compressedImageFile = await compressFile(selectedImage);
setCompressedImage(compressedImageFile);
} catch (error) {
console.log({ error });
}
}
};
return (
<section className={styles.container}>
<FileInput handleOnChange={handleOnChange} />
<article className={styles.previewer}>
<aside>
<div className={styles["button-wrapper"]}>
{selectedImage && (
<button
diasbled={isCompressing}
onClick={handleCompressFile}
className={styles.button}
>
{isCompressing ? "Compressing..." : " Compress Image"}
</button>
)}
</div>
</aside>
</article>
</section>
);
}
Code language: JavaScript (javascript)
In the code above, we start by importing the files and packages we’ll be needing in this component. Don’t worry; we will still create some of these files. In our App
component, we defined three state variables and two functions. The selectedImage
and compressedImage
state variables are responsible for storing the selected image file and its compressed version, and isCompressing
keeps track of the loading state during compression.
The function handlechange
will be triggered whenever a new file (image) is selected. It is passed as props to the fileInput
component.
The asynchronous function handleCompress
then takes the selected image file and compresses it using the compressImage
method. If successful, it stores the resulting image in the compressedImage
state variable. We passed the handleCompress
method as an event handler to a button’s onClick
click event in the JSX returned, and it is rendered only when an image is selected.
Run this command in your terminal to see what we have so far:
npm run dev
We can select an image and compress it with what we have working currently, but we can’t see the result (compressed image). Let’s define the component for previewing an image. Add the following to your imagePreviewer.js
file:
import { useEffect, useState } from "react";
import styles from "../styles/Home.module.css";
export default function ImagePreviewer({ imageFile, children }) {
const [imageURL, setImageURL] = useState();
useEffect(() => {
if (!imageFile) return;
const url = URL.createObjectURL(imageFile);
setImageURL(url);
return () => url && URL.revokeObjectURL(url);
}, [imageFile]);
return imageFile ? (
<>
<div className={styles["image-wrapper"]}>
<img src={imageURL} alt="" />
</div>
<p>{`${(imageFile.size / 1024 / 1024).toFixed(2)} MB`}</p>
</>
) : null;
}
Code language: JavaScript (javascript)
The component ImagePreviewer
accepts an image file object as props and only renders the JSX if the Object is passed. We also defined some state variables which will be used to hold the image URL and set the initial value to an empty string.
The component ImagePreviewer
accepts an image file object as props and only renders JSX if the Object is passed. We can’t render the file object directly in an image tag, so we’re first converting it to a URL by calling createObjectURL
. We also defined a state variable that will store the image URL and set the initial value to an empty string.
In the clean-up function returned from the useEffect
hook, we clear any existing memory allocated to previous image objects during re-renders to avoid memory leaks.
In the return statement for the component, we have an img
tag that accepts the image URL as its src
. We are also displaying the size of the image which is stored on the image object.
Next, let’s head over to our App
component and update the logic to include this component.
// App.js
export default function App() {
//...
return (
<section className={styles.container}>
<FileInput handleOnChange={handleOnChange} />
<article className={styles.previewer}>
<aside>
<ImagePreviewer imageFile={selectedImage} />
//...
</aside>
<aside>
<ImagePreviewer imageFile={compressedImage} />
</aside>
</article>
</section>
);
}
Code language: JavaScript (javascript)
In the code above, we added two instances of the ImagePreviewer
component. The first one receives the selected image, and the second one gets the compressed image.
If we run our app now and compress an image, we should see both versions.
Now that we’ve compressed the image let’s implement a download function for downloading the compressed image. Add this download function to the helpers.js
file:
export function download(file) {
const url = window.URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "");
link.click();
window.URL.revokeObjectURL(url);
}
Code language: JavaScript (javascript)
This download
function expects an image file object. Internally, it creates a URL representation of this Object and sets it to the href
attribute of an anchor tag to make the file downloadable.
Let’s now head back to our App
component and make use of this function.
export default function App() {
const handleOnChange = (event) => {
setSelectedImage(event.target.files[0]);
};
// add this download function
const handleDownload = () => {
download(compressedImage);
};
const handleCompressFile = async () => {
//...
};
return (
<section className={styles.container}>
<FileInput handleOnChange={handleOnChange} />
<article className={styles.previewer}>
<aside>
<ImagePreviewer imageFile={selectedImage} />
/...
</aside>
<aside>
<ImagePreviewer imageFile={compressedImage} />
{/* add the following */}
<div className={styles["button-wrapper"]}>
{compressedImage && (
<>
<button onClick={handleDownload} className={styles.downloadBtn}>
Download
</button>
</>
)}
</div>
</aside>
</article>
</section>
);
}
Code language: HTML, XML (xml)
In the return statement of our App
component, we added a button with an onClick
event that accepts the handleDownload
function. The handleDownload
function calls the download
function, which receives the compressed image. If we run our app now, we should be able to download a compressed image.
We will be uploading images to Cloudinary. If you don’t have a Cloudinary account yet, you can sign up here for a free account. Log in after creating your account, and on your dashboard page, you should see your credentials (cloud name, etc.).
At the root of your project, create a .env
file that will be used to hold your Cloudinary credentials as environment variables. Add the following to your .env
file and update their values:
CLOUDINARY_API_Key=INSERT-YOUR-CLOUDINARY-API-KEY-HERE
CLOUDINARY_API_SECRET=INSERT-YOUR-CLOUDINARY-API-SECRET-HERE
We’ll leverage Next.js API routes to create a serverless function that will house the logic that handles image upload to Cloudinary. Create a file called upload.js
in the api
folder in your pages
directory and add the following to it:
import cloudinary from "cloudinary";
const cloudinaryConfig = {
cloud_name: "INSERT-CLOUD-NAME-HERE",
api_key: process.env.CLOUDINARY_API_Key,
api_secret: process.env.CLOUDINARY_API_SECRET,
secure: true,
};
cloudinary.v2.config(cloudinaryConfig);
async function handler(req, res) {
try {
let { file } = req.body;
const cloudinaryResponse = await cloudinary.v2.uploader.upload(file);
console.log(cloudinaryResponse);
return res.json({ message: "upload successful" });
} catch (error) {
console.log({ err: error.message });
res.json({ message: "oopsie an error occured" });
}
}
export default handler;
export const config = {
api: {
bodyParser: {
sizeLimit: "3mb",
},
},
};
Code language: JavaScript (javascript)
In the code above, we are importing Cloudinary and using it to configure our Credentials. Next, we define a serverless function that would handle HTTP requests made to this file. It receives the image file as a base64 URL from the request body and uploads it using the Cloudinary SDK. If it succeeds, it gets an upload response and prints it to the console, but if it fails, it sends an error message.
By default, Next.js API routes allow the request body to be a maximum size of 1MB, but the incoming image data in the request body may be large. To ensure that our serverless function doesn’t give us errors when parsing the request body, we also export a configuration object that increases it to 3MB.
Cloudinary supports different file source options during upload. Our implementation uses the base64 URL string option. See other options here.
Before making any request to our serverless function, we need to define a helper method to parse our compressed file to base64.
Add the following to your helpers.js
file:
import imageCompression from "browser-image-compression";
//...
export function compressFile(imageFile, options = defaultOptions) {
return imageCompression(imageFile, options);
}
export function download(file) {
//...
}
// Add this
export function readFileAsBase64(file) {
return imageCompression.getDataUrlFromFile(file);
}
Code language: JavaScript (javascript)
Now, update your index.js
file with the following:
export default function App() {
// Add this
const [isLoading, setIsLoading] = useState(false);
const handleOnChange = (event) => {
setSelectedImage(event.target.files[0]);
};
const handleDownload = () => {
download(compressedImage);
};
const handleCompressFile = async () => {
//...
};
// Add this function
const handleUpload = async () => {
const fileInBase64 = await readFileAsBase64(compressedImage);
setIsLoading(true);
try {
const res = await axios.post("/api/upload", {
file: fileInBase64,
});
alert(res.data.message);
} catch (error) {
console.log({ error });
} finally {
setIsLoading(false);
}
};
return (
<section className={styles.container}>
//...
<aside>
<ImagePreviewer imageFile={compressedImage} />
<div className={styles["button-wrapper"]}>
{compressedImage && (
<>
<button onClick={handleDownload} className={styles.downloadBtn}>
Download
</button>
// add this
<button
className={styles.uploadBtn}
onClick={handleUpload}
disabled={isLoading}
>
{isLoading ? "uploading..." : "upload"}
</button>
</>
)}
</div>
</aside>
</article>
</section>
);
}
Code language: PHP (php)
In the code above, we added a state variable to keep track of the loading state. We defined a function handleUpload
that first toggles our loading state, and then it uses the helper method readFileAsBase64
to convert the compressed image to base64.
After that, it sends it to the serverless function via HTTP using the axios post method. If everything works fine, we display a notification confirming a successful upload or an error if anything goes wrong.
To view uploaded images, log in to your Cloudinary account and head over to your media library.
Find the complete project here on GitHub.
Some resources you may find helpful: