How Node.js Sharp Uses Libavif Under the Hood
This article explores the technical pipeline that enables Node.js
image processing libraries, specifically sharp, to compress
and manipulate AVIF images. It details the multi-layered architecture
starting from JavaScript, moving through C++ bindings (N-API) and the
high-performance libvips library, and finally executing the
core encoding and decoding tasks via the C-based libavif
library.
The Architectural Pipeline
To process AVIF images, sharp does not execute image
compression directly in JavaScript. Because JavaScript is
single-threaded and not optimized for heavy mathematical computations,
sharp acts as a wrapper around a native C++ engine. The
data flows through four distinct layers:
- JavaScript Layer (
sharp): The developer calls methods likesharp('input.jpg').avif().toFile('output.avif'). - C++ Binding Layer (N-API / Node-API): Bridges the Node.js runtime and the native C++ library, passing Javascript buffers and configuration options to native memory.
- Image Processing Engine (
libvips): A fast, multi-threaded image processing library that manages memory, handles resizing, and orchestrates image operations. - Codec Layer (
libavif): The specialized library responsible for parsing the AVIF container and encoding/decoding the underlying AV1 video frames.
The Role of libvips as the Mediator
At the core of sharp is libvips. When
sharp is installed, it downloads pre-compiled native
binaries of libvips tailored to the host operating
system.
libvips contains a writer and reader plugin system. When
sharp requests an AVIF output, libvips
identifies the .avif format and routes the raw image pixel
buffer to its foreign save plugin,
vips_foreign_save_avif. This plugin acts as the direct
interface to libavif.
How libavif Handles the Heavy Lifting
libavif is a C library written by the Alliance for Open
Media (AOMedia) designed to encode and decode AVIF files. AVIF is
technically an image format that wraps AV1 video frames inside a HEIF
(High Efficiency Image File Format) ISO base media file format
container.
When libavif receives pixel data from
libvips, it performs two primary tasks:
1. Interfacing with AV1 Codecs
libavif itself does not compress the raw pixels into AV1
format; it delegates this to an underlying AV1 codec. * Encoding
(Compression): libavif typically uses
libaom (the reference encoder) or
SVT-AV1 to compress the spatial image data into an AV1
bitstream. * Decoding (Decompression): When reading an
AVIF, libavif often utilizes dav1d, a
highly optimized, fast AV1 decoder, to translate the compressed
bitstream back into raw RGB/RGBA pixels.
2. Packaging the HEIF Container
Once the AV1 encoder produces the compressed payload,
libavif packages this bitstream into the standard HEIF
container. This step includes writing metadata such as color profiles
(ICC profiles), Exif data, and alpha channel configuration
(transparency) into the file structure.
Memory Management and Threading
To prevent image processing from blocking the main Node.js event
loop, sharp and libvips utilize the libuv
thread pool.
When an AVIF operation begins, the JavaScript thread hands the task
off to a background worker thread. libvips allocates memory
outside of the V8 JavaScript engine’s garbage-collected heap.
libavif then utilizes multi-threading (leveraging CPU cores
via the AV1 codec) to compress the image in parallel. Once the
compression is complete, the native memory is freed, and the final AVIF
buffer is passed back to Node.js as a standard JavaScript
Buffer object.