Skip to main content
Robert Michalski

Resize and convert animated GIF and WebP images using Sharp in Node.js

In this post we'll first resize animated GIFs and WebP files to reduce their filesize somewhat, then we'll go through how to convert animated GIFs to WebP format to reduce the filesize even more. It's possible to convert from animated WebP to GIF as well.

Converting animated GIF images to WebP can actually increase the filesize for some images. I have experienced it a couple of times, however it's very rare, just keep the smaller GIF files.

The information is best suited for developers that are familiar with Node.js and have some experience converting images with the Sharp library already. I'll improve the guide over time to make it more beginner-friendly.

What is an animated GIF? #

The Graphics Interchange Format, or GIF for short, was created in 1987 which is 37 years ago (2024). It supports up to 256 colors and multiple images with delays between them, e.g. animations.

GIFs are suitable for simple sharp lined logos with few colors (up to 256), that's where they shine with small file size and simplicity. Patents have long since expired making the GIF format free to use.

Why bother optimizing animated GIFs? #

In this day and age, animated GIFs are often used for memes and animated reactions in messaging platforms and software, often for complex images like photos.

An animated GIF can contain tens of frames, each is an image taking up several hundred kilobytes, which makes the total size up to several megabytes.

They're used very frivolously in Slack, Facebook, Instagram and webpages, causing tens of megabytes to be downloaded millions of times.

A lot of bandwidth, storage and electricity to transfer all those images across the world can be reduced by making the size of animated GIFs smaller. Even small file size reductions multiplied millions or billions of times quickly add up and make a real difference.

Resizing the images to an optimal size can reduce the file size somewhat, as each frame, tens of them, becomes smaller. A couple of percent reduction for each frame quickly adds up to some nice file size savings.

Converting them to more modern file formats, better suited for complex images, like WebP, can reduce the file size by as much as 70-90%, for example a GIF at around 2 MB can be reduced to a 200 KB WebP file. Browser support for WebP is at over 95%, all modern browsers have supported it for several years.

How to optimize animated GIFs using Node.js #

There's lots of ways to resize and convert images, online-converters, dedicated software, 3rd-party services etc. In this post we'll be using Node.js with the excellent and very performant Sharp module to resize animated GIFs and also convert them to WebP.

Create Node.js project and install Sharp #

Initiate a new Node.js project, add and install sharp as a dependency and create a source folder inside the project folder.

mnkdir optmizing-animated-gifs
cd optmizing-animated-gifs

npm init
npm i sharp p-map fs-extra --save

mkdir source

Download some example animated GIFs from the Internet to the source folder inside the project.

How to resize an animated GIF or WebP #

Create an index.js file in the project folder in your favorite code editor (VSCode, Vim, etc) or IDE (WebStorm, PHPStorm, etc).

import Path from 'node:path'
import Fs from 'fs-extra'
import pMap from 'p-map'
import Sharp from 'sharp'

const DESIRED_MAX_WIDTH = 100
const DESIRED_MAX_HEIGHT = 100
const CONCURRENT_IMAGES_TO_PROCESS = 5

(async () => {
    const sourceDir = Path.resolve('./source')
    const destiantionDir = Path.resolve('./destination')

    const files = (await Fs.readdir(sourceDir))
        .filter(
            file => !file.startsWith('.') && // filter out hidden files (beginning with .)
                //Path.extname(file) !== '' && // files with no extension
                ['gif', 'webp'].includes(file.split('.').pop().toLowerCase()) // only pass through files with .webp or .gif file extenstion
        )

    const converted = await pMap(files, async fileName => {
        const filePath = Path.join(sourceDir, fileName)
        const sharpImage = sharp(filePath, { animated: true, pages: -1 }) // supports animated gif and webp images

        const imageMeta = await sharpImage.metadata()
        const { width, height: heightAllPages, size, loop, pages, pageHeight, delay } = imageMeta
        const height = pageHeight || (heightAllPages / pages) // pageHeight usually only exists for gif, not webp

        const resized = sharpImage.resize({
            width: DESIRED_MAX_WIDTH,
            height: DESIRED_MAX_HEIGHT * pages,
            fit: sharp.fit.inside
        })
        const destiantionPath = Path.join(destiantionDir, fileName)
        return resized.toFile(destiantionDir)
    }, { concurrent: CONCURRENT_IMAGES_TO_PROCESS, stopOnError: false })

    console.log(converted)
})

How to convert an animated GIF to WebP #

By converting an animated gif to webp usually a 60-80% file size reduction can be achieved.

const sharpImage = sharp(filePath, { animated: true, pages: -1 }) // supports animated gif and webp images

const imageMeta = await sharpImage.metadata()
const { paletteBitDepth, loop, delay } = imageMeta
const colors = paletteBitDepth > 0
    ? Math.pow(2, paletteBitDepth)
    : 256 // default

const options = {
    webp: {
        loop,
        delay,
        quality: 60, // 0-100
        lossless: false,
        nearLossless: false,
        smartSubsample: false,
        effort: 4, // 0-6, effort 6 takes a very long time just to save a little bit of space
        loop: 0, // animations loop forever, default
        //minSize: true, // prevent use of animation key frames to minimise file size (slow), little effect on size
        //mixed: true, // allow mixture of lossy and lossless animation frames (slow), little effect on size
        force: true
    },
    gif: {
        loop, // animations loop forever, default
        delay,
        colors: colors,
        //colours: colors, // alias of colours
        //quality: 50, // 0-100
        //reoptimise: true,
        //reoptimize: true,
        effort: 7, // 0-10
        force: true
    }
}

const result = await sharpImage
    //.withMetadata() // optional to keep image metadata
    .webp(options.webp)
    //.gif(options.gif)
    //.toFile(destiantionPath) // output to file
    .toBuffer({ resolveWithObject: true })

console.log(result)