DEV Community

Aaron Decker
Aaron Decker

Posted on Originally published at ard.ninja

PureJsImage: Low-Memory Image Processing in Pure TypeScript for Node.js, Browsers, and AWS Lambda

PureJsImage is an open-source image processing library written in strict TypeScript. It runs in Node.js and modern browsers, has no runtime dependency tree, and focuses on low peak memory use for image resize, crop, conversion, scientific raster, geographic raster, and whole-slide workflows.

The project started because a small uploaded JPEG could push one of my AWS Lambda image jobs past its memory limit. The compressed file was only a few megabytes, but decoding it into one or more full-size bitmaps was enough to make the process use more than 1 GiB.

That problem led me to build first-party JavaScript codecs that can work in bounded rows, scanlines, strips, tiles, regions, or compact coefficient storage when the format allows it.

Quick links

Install it with:

npm install purejsimage
Enter fullscreen mode Exit fullscreen mode

PureJsImage v0.16.0 requires Node.js 22 or newer. Browser applications use the purejsimage/browser entry point. The public APIs are still pre-1.0, so they may receive breaking refinements.

Why image processing can consume so much memory

Compressed file size is not the same as decoded image size.

A 6000 × 4000 image has 24 million pixels. One RGBA bitmap for that image needs about 96 MB before accounting for the source bytes, decoder state, resize intermediates, output buffers, JavaScript runtime, and allocator overhead. A pipeline that retains several full-frame buffers can quickly exceed a 256 MiB or 512 MiB serverless limit.

The usual sequence can look like this:

decode the complete source image
→ create a full crop buffer
→ create a full resize buffer
→ convert color data
→ encode the result
Enter fullscreen mode Exit fullscreen mode

PureJsImage tries to reduce the amount of work and memory before optimizing individual instructions. Depending on the codec and operation, it can:

  • push crop and resize requirements toward the decoder
  • retain bounded MCU rows, scanlines, strips, tiles, or regions
  • reuse typed-array scratch buffers
  • avoid duplicate full-frame buffers at codec boundaries
  • read only selected byte ranges from remote TIFF, whole-slide, scientific, and geographic sources
  • preserve native scientific sample types until an application explicitly renders them

Some formats and operations still require larger state. PureJsImage documents those cases instead of presenting every path as constant-memory.

Current low-memory benchmark results

The current web-codec snapshot is dated August 18, 2026. It contains 122 validated passes and 25 explicit unsupported rows across JPEG, PNG, WebP, TIFF, and AVIF workflows, with no invalid outputs or benchmark errors in that snapshot.

On the shared 24-megapixel photo pipeline, the default TypeScript path used 84.9% less absolute peak RSS than Jimp: 180.1 MiB versus 1189.3 MiB. That ratio belongs to one pinned corpus, harness, environment fingerprint, and date. The performance page publishes the charts, methodology, and result artifacts.

Web codec benchmark speed chart. Lower median wall time is better.

Median wall time from the current validated web-codec snapshot. Sharp uses native libvips and is included as a native reference, not as a pure JavaScript implementation.

Web codec benchmark peak RSS chart. Lower peak RSS is better.

Absolute process peak RSS from the same validated snapshot.

I also ran separately dated AWS Lambda measurements. A 256 MiB Lambda completed all four measured 4000 × 3000 resize and conversion workflows with 121–156 MiB maximum use. Memory was only part of the sizing decision: the warm JPEG-to-WebP workflow took 10,601 ms at 256 MiB, 5,261 ms at 512 MiB, and 2,533 ms at 1024 MiB while maximum use stayed near 120–122 MiB. AWS allocates more CPU with more Lambda memory, so a larger tier can still be the right choice for latency.

See the dated AWS Lambda results and Lambda usage guide before choosing a production memory tier. Re-measure with your images, concurrency, architecture, and latency requirements.

If native libvips is available and maximum throughput is the main goal, use Sharp. PureJsImage is aimed at projects where portable TypeScript, browser parity, explicit dependency boundaries, native scientific data, or lower-memory JavaScript execution are important.

Pure TypeScript by default, optional WASM by explicit import

The ordinary codec implementations are first-party TypeScript. The published package has no runtime dependency tree, native addon, required system executable, or automatic WASM download.

JPEG and PNG also have optional WASM accelerators. They live behind separate imports and explicit registration. Applications that do not opt in continue to use the TypeScript implementations.

You can register only the codecs an application needs:

import { createImageLibrary } from "purejsimage";
import { jpegCodec } from "purejsimage/codecs/jpeg";
import { pngCodec } from "purejsimage/codecs/png";

const images = createImageLibrary({ codecs: [jpegCodec, pngCodec] });
const image = await images.open("input.jpg");

await image
  .autoOrient()
  .resize({ width: 1200, withoutEnlargement: true })
  .jpeg({ quality: 80, background: "#ffffff" })
  .toFile("output.jpg");
Enter fullscreen mode Exit fullscreen mode

In a browser, inputs can be File, Blob, ArrayBuffer, Uint8Array, fetched bytes, or an explicit ImageSource. Browser outputs can be Uint8Array, Blob, or an explicit ImageSink.

Complete ordinary image codec support

PureJsImage currently has 14 stable ordinary codecs. The generated codec matrix contains the detailed feature and limitation checklists.

Format Decode Encode Current boundary
JPEG Yes Yes Optional explicit WASM accelerator
PNG Yes Yes Optional explicit WASM accelerator
WebP Yes Yes Static images
BMP Yes Yes Portable TypeScript codec
TIFF Yes Yes Includes BigTIFF plus GeoTIFF and Cloud Optimized GeoTIFF workflows
GIF Yes No Static decode or explicitly selected frame 0
ICO Yes No Selects and decodes embedded icon images
JPEG 2000 / JP2 Yes No Decode only
AVIF Yes Limited Broad decode support and a documented encode subset
JPEG XL Limited No Documented decode subset with explicit rejection outside it
Radiance HDR / RGBE Yes Yes HDR decode and encode
QOI Yes Yes Decode and encode
Netpbm and PFM Yes Yes PBM, PGM, PPM, PAM, and PFM
TGA / TARGA Yes Yes Decode and encode

“Limited” means the project implements a useful, tested subset and rejects unsupported syntax explicitly. An unsupported file should not silently become plausible-looking corrupted output.

Experimental HEIF and HEIC support

HEIF/HEIC decoding is experimental and available only through purejsimage/codecs/experimental/heic. It is excluded from allCodecs, is never selected automatically, and has no encoder.

HEIC commonly carries HEVC/H.265 content that may be subject to third-party patent rights. The project code is MIT licensed, but that license grants no third-party patent rights. Users and distributors, including commercial products and services, must evaluate their own licensing obligations.

Scientific, microscopy, medical, and whole-slide image support

Ordinary image libraries usually convert everything into RGB or RGBA. That is a poor default for a hyperspectral cube, electron microscopy dataset, medical volume, diffraction pattern, elevation grid, or pathology slide.

PureJsImage has a separate scientific dataset API that keeps labeled axes, calibration, native numeric sample types, and selected-resource semantics. An application chooses when and how to map those samples into display pixels.

The v0.16.0 package exposes 33 scientific readers:

  • Common raster and whole-slide (9): PNG, JPEG, WebP, BMP, JPEG 2000/JP2, TIFF, OME-TIFF, OME-Zarr, and Aperio SVS
  • Electron microscopy (7): Gatan DigitalMicrograph DM3/DM4, FEI/Thermo TIA SER, TIA EMI, NCEM EMD 0.2, FEI/Thermo Velox EMD, NanoMegas ASTAR blockfile, and Quantum Detectors Merlin MIB
  • AFM, SPM, and surface metrology (5): Gwyddion Simple Field, Nanonis SXM, Igor Binary Wave v5, Digital Surf SUR/PRO, and X3P surface exchange
  • Medical and volume interchange (5): MRC/CCP4, NRRD, MetaImage MHD/MHA, NIfTI-1/2, and DICOM Part 10 Image
  • Spectroscopy and detector interchange (6): ENVI, FITS, CBF/imgCIF, Lispix RPL/RAW, EMSA/MAS spectrum, and ANG/CTF orientation maps
  • Raw numeric interchange (1): NumPy NPY

Direct-range readers can request only the source spans needed for metadata, a region, a spectrum, a volume plane, or a whole-slide tile. The exact per-reader imports and boundaries are listed in the scientific format reference.

PureJsImage Scientific Explorer rendering native ENVI hyperspectral samples in the browser

The Scientific Raster Explorer maps native ENVI samples to display pixels in the browser. It does not rewrite the source cube as RGB.

PureJsImage whole-slide viewer reading an Aperio SVS pathology slide in the browser

The whole-slide demo reads visible pyramid tiles from a 1.98 GiB Aperio SVS file through HTTP Range requests.

There is also PureJsImage Lab for exploring supported microscopy and scientific files locally in the browser.

Geographic raster support

PureJsImage also exposes lazy geographic raster readers through purejsimage/geo/readers. The current surface includes:

  • GeoTIFF and Cloud Optimized GeoTIFF
  • GeoZarr v2 and v3, including supported v3 shards and multiscales
  • TIFF, JPEG, and PNG with world files
  • georeferenced ENVI
  • Esri ASCII Grid
  • SRTM HGT
  • classic NetCDF CDF-1 and CDF-2 with regular rectilinear CF coordinates

These readers use a shared geographic raster contract for spatial reference evidence, affine transforms, bounds, nodata, selectable axes, region reads, and reprojection where supported. The Geo demos and documentation show the current boundaries.

Compatibility is part of the implementation

A new image codec needs evidence against real files, not only hand-built happy-path fixtures. PureJsImage uses pinned corpora, including the excellent Imazen codec corpus, plus focused hostile-input, oracle, browser, and round-trip checks.

Benchmark rows count only when the output passes the relevant correctness or quality gate. Unsupported syntax is recorded as unsupported. Invalid output is a failed result regardless of speed.

The benchmark reports include more than wall time. Depending on the suite, they also record:

  • absolute peak RSS
  • output validity and quality evidence
  • cold and warm behavior
  • initialization time
  • external and ArrayBuffer memory
  • source request count and bytes read
  • overfetch for range-backed data
  • package and bundle footprint

That evidence is published in the benchmark documentation and on the performance page.

Why I am building it

I wanted a JavaScript image library for a narrow practical job: resize, crop, convert, and inspect images without creating avoidable memory pressure in Lambda or a low-memory browser.

Once the codec foundation worked, TIFF led me toward scientific and medical imaging. My bachelor’s degree is in chemistry, and I worked in a 3D NMR lab in college. I have used spectroscopy instruments and know how awkward it can be to move real scientific data between specialized tools.

The project has grown into a portable TypeScript implementation for ordinary images, scientific rasters, microscopy, medical volumes, whole-slide pathology, and geographic rasters. It is MIT licensed and open to contributions.

If you want to help, start with the GitHub repository, read the contributing guide, try the browser demos, or open an issue with a real file and a clearly described unsupported workflow.

The original version of this announcement is on ard.ninja.

Top comments (0)