TL;DR
image-size is the lightweight image dimension detector — reads just enough bytes to determine width, height, and format, supports 15+ formats, no native dependencies. probe-image-size streams just the header bytes — works with URLs and streams, minimal data transfer, detects dimensions without downloading the full image. sharp's metadata uses libvips to read full image metadata — dimensions, color space, EXIF, ICC profiles, but requires native binaries. In 2026: image-size for local files (simple, fast), probe-image-size for remote URLs (minimal download), sharp metadata when you're already using sharp for image processing.
Key Takeaways
- image-size: ~15M weekly downloads — pure JS, sync/async, 15+ formats, zero native deps
- probe-image-size: ~3M weekly downloads — streams headers only, URL support, minimal bandwidth
- sharp metadata: ~10M weekly downloads — full metadata (EXIF, ICC), requires libvips native binary
- All three detect width, height, and format — different trade-offs
- image-size reads minimal bytes from buffers/files — fastest for local files
- probe-image-size is ideal for remote images — downloads only header bytes
image-size
image-size — lightweight dimension detection:
Basic usage
import sizeOf from "image-size"
// From a file path (sync):
const dimensions = sizeOf("./public/logo.png")
console.log(dimensions)
// → { width: 512, height: 512, type: "png" }
// From a buffer:
import { readFileSync } from "node:fs"
const buffer = readFileSync("./public/hero.jpg")
const size = sizeOf(buffer)
// → { width: 1920, height: 1080, type: "jpg" }
Async usage
import { imageSize } from "image-size"
import { readFile } from "node:fs/promises"
// Async with callback:
imageSize("./public/logo.png", (err, dimensions) => {
if (err) throw err
console.log(dimensions.width, dimensions.height)
})
// With promises:
const buffer = await readFile("./public/hero.jpg")
const dimensions = imageSize(buffer)
console.log(`${dimensions.width}x${dimensions.height}`)
// → "1920x1080"
Supported formats
import sizeOf from "image-size"
// Supports 15+ image formats:
sizeOf("photo.jpg") // → { width, height, type: "jpg" }
sizeOf("logo.png") // → { width, height, type: "png" }
sizeOf("icon.gif") // → { width, height, type: "gif" }
sizeOf("image.webp") // → { width, height, type: "webp" }
sizeOf("vector.svg") // → { width, height, type: "svg" }
sizeOf("photo.avif") // → { width, height, type: "avif" }
sizeOf("image.tiff") // → { width, height, type: "tiff" }
sizeOf("icon.ico") // → { width, height, type: "ico", images: [...] }
sizeOf("image.bmp") // → { width, height, type: "bmp" }
sizeOf("image.psd") // → { width, height, type: "psd" }
sizeOf("image.heif") // → { width, height, type: "heif" }
Multi-size images (ICO)
import sizeOf from "image-size"
// ICO files contain multiple sizes:
const ico = sizeOf("favicon.ico")
console.log(ico)
// → {
// width: 32, height: 32, type: "ico",
// images: [
// { width: 16, height: 16 },
// { width: 32, height: 32 },
// { width: 48, height: 48 },
// ]
// }
Use case: Image validation
import sizeOf from "image-size"
function validateImage(buffer: Buffer, options: {
maxWidth?: number
maxHeight?: number
allowedFormats?: string[]
}) {
const { width, height, type } = sizeOf(buffer)
if (options.maxWidth && width > options.maxWidth) {
throw new Error(`Image width ${width} exceeds max ${options.maxWidth}`)
}
if (options.maxHeight && height > options.maxHeight) {
throw new Error(`Image height ${height} exceeds max ${options.maxHeight}`)
}
if (options.allowedFormats && !options.allowedFormats.includes(type)) {
throw new Error(`Format "${type}" not allowed`)
}
return { width, height, type }
}
probe-image-size
probe-image-size — stream-based detection:
From URL (minimal download)
import probe from "probe-image-size"
// Probe a remote image — only downloads header bytes:
const result = await probe("https://pkgpulse.com/og/react-vs-vue.png")
console.log(result)
// → {
// width: 1200,
// height: 630,
// type: "png",
// mime: "image/png",
// wUnits: "px",
// hUnits: "px",
// }
// For a 5MB image, probe might download only ~100 bytes
From stream
import probe from "probe-image-size"
import { createReadStream } from "node:fs"
// From a file stream:
const stream = createReadStream("./public/hero.jpg")
const result = await probe(stream)
// → { width: 1920, height: 1080, type: "jpg", mime: "image/jpeg" }
// Stream is NOT fully consumed — only reads enough bytes for detection
From buffer
import probe from "probe-image-size"
// From a buffer:
const buffer = await readFile("./public/logo.png")
const result = probe.sync(buffer)
// → { width: 512, height: 512, type: "png", mime: "image/png" }
With HTTP options
import probe from "probe-image-size"
// Custom HTTP options for remote probing:
const result = await probe("https://private-cdn.com/image.jpg", {
headers: {
Authorization: "Bearer token",
"User-Agent": "PkgPulse/1.0",
},
timeout: 5000,
retries: 2,
follow_max: 3, // Max redirects
})
Use case: OG image validation
import probe from "probe-image-size"
async function validateOGImage(url: string) {
try {
const { width, height, type } = await probe(url)
const issues: string[] = []
// OG images should be 1200x630:
if (width < 1200) issues.push(`Width ${width} < 1200`)
if (height < 630) issues.push(`Height ${height} < 630`)
if (width / height > 2) issues.push("Aspect ratio too wide")
// Preferred formats:
if (!["png", "jpg", "webp"].includes(type)) {
issues.push(`Format "${type}" not recommended for OG images`)
}
return { valid: issues.length === 0, width, height, type, issues }
} catch (err) {
return { valid: false, issues: [`Failed to probe: ${err.message}`] }
}
}
sharp metadata
sharp — full image metadata:
Basic metadata
import sharp from "sharp"
// Read metadata from a file:
const metadata = await sharp("./public/hero.jpg").metadata()
console.log(metadata)
// → {
// width: 1920,
// height: 1080,
// format: "jpeg",
// space: "srgb",
// channels: 3,
// depth: "uchar",
// density: 72,
// chromaSubsampling: "4:2:0",
// isProgressive: false,
// hasProfile: true,
// hasAlpha: false,
// orientation: 1,
// exif: <Buffer ...>,
// icc: <Buffer ...>,
// }
From buffer
import sharp from "sharp"
const buffer = await readFile("./uploads/photo.jpg")
const { width, height, format, size } = await sharp(buffer).metadata()
console.log(`${width}x${height} ${format} (${size} bytes)`)
// → "1920x1080 jpeg (245760 bytes)"
EXIF data
import sharp from "sharp"
const metadata = await sharp("photo.jpg").metadata()
if (metadata.exif) {
// Parse EXIF with exif-reader:
const exifReader = await import("exif-reader")
const exif = exifReader.default(metadata.exif)
console.log(exif.Image?.Make) // "Apple"
console.log(exif.Image?.Model) // "iPhone 15 Pro"
console.log(exif.Photo?.DateTimeOriginal) // Date object
console.log(exif.GPSInfo?.GPSLatitude) // GPS coordinates
}
Stats (pixel analysis)
import sharp from "sharp"
// Get pixel statistics:
const stats = await sharp("image.png").stats()
console.log(stats)
// → {
// channels: [
// { min: 0, max: 255, sum: 12345678, mean: 128.5, ... }, // R
// { min: 0, max: 255, sum: 12345678, mean: 126.3, ... }, // G
// { min: 0, max: 255, sum: 12345678, mean: 130.1, ... }, // B
// ],
// isOpaque: true,
// dominant: { r: 45, g: 120, b: 200 },
// }
Feature Comparison
| Feature | image-size | probe-image-size | sharp metadata |
|---|---|---|---|
| Dimensions | ✅ | ✅ | ✅ |
| Format detection | ✅ | ✅ | ✅ |
| MIME type | ❌ | ✅ | ✅ |
| EXIF data | ❌ | ❌ | ✅ |
| ICC profiles | ❌ | ❌ | ✅ |
| Color space | ❌ | ❌ | ✅ |
| URL probing | ❌ | ✅ | ❌ |
| Stream support | ❌ | ✅ | ✅ |
| Sync API | ✅ | ✅ | ❌ |
| Native deps | ❌ (pure JS) | ❌ (pure JS) | ✅ (libvips) |
| Formats | 15+ | 10+ | 15+ |
| Size | ~20KB | ~15KB | ~50MB (with libvips) |
| Weekly downloads | ~15M | ~3M | ~10M |
When to Use Each
Use image-size if:
- Need quick width/height from local files or buffers
- Want zero native dependencies (pure JS)
- Building upload validation or image catalogs
- Need sync API for simple scripts
Use probe-image-size if:
- Need dimensions of remote images (URLs)
- Want to minimize bandwidth (only downloads header bytes)
- Building OG image validators or link previews
- Working with streams
Use sharp metadata if:
- Already using sharp for image processing
- Need EXIF data, ICC profiles, or color space info
- Need pixel statistics (dominant color, etc.)
- Building image processing pipelines
Migration Guide
Adding image dimensions to Next.js Image components
A common use case for image-size is generating the width and height props for Next.js <Image> components at build time, which Next.js requires to avoid layout shift:
import sizeOf from "image-size"
import path from "path"
// At build time (in a script or getStaticProps):
async function getImageDimensions(src: string): Promise<{ width: number; height: number }> {
// Local images in public directory:
if (src.startsWith("/")) {
const filePath = path.join(process.cwd(), "public", src)
const { width, height } = sizeOf(filePath)
return { width: width!, height: height! }
}
// Remote images (use probe-image-size):
const probe = await import("probe-image-size")
const { width, height } = await probe.default(src)
return { width, height }
}
// Usage in a blog post:
const { width, height } = await getImageDimensions("/blog/hero.png")
// → pass as props to <Image width={width} height={height} src="/blog/hero.png" />
From manual dimension tracking to image-size
If you're currently storing image dimensions in your content metadata manually (error-prone), automate it with image-size during content build:
import sizeOf from "image-size"
import { glob } from "glob"
import path from "path"
// Generate an image manifest with dimensions:
async function buildImageManifest() {
const images = await glob("public/**/*.{png,jpg,webp,avif}", { cwd: process.cwd() })
const manifest: Record<string, { width: number; height: number; type: string }> = {}
for (const imgPath of images) {
const fullPath = path.join(process.cwd(), imgPath)
const { width, height, type } = sizeOf(fullPath)
const key = imgPath.replace("public", "")
manifest[key] = { width: width!, height: height!, type: type! }
}
return manifest
}
// Save to .image-manifest.json at build time and import where needed
Switching from URL fetching to probe-image-size
If you're fetching remote images fully just to get dimensions, switch to probe:
// Before: downloading entire image just for dimensions
async function getRemoteImageSize(url: string) {
const response = await fetch(url)
const buffer = Buffer.from(await response.arrayBuffer())
// May download 5MB just to read 100 bytes of header
return sizeOf(buffer)
}
// After: probe downloads only header bytes
import probe from "probe-image-size"
async function getRemoteImageSize(url: string) {
const { width, height, type } = await probe(url)
// Downloads ~100-500 bytes instead of the full file
return { width, height, type }
}
Security Considerations in Image Metadata Parsing
Image metadata parsing is a frequently overlooked attack surface. Malformed image files — sometimes called "image bombs" or "decompression bombs" — can cause parsers to allocate excessive memory or crash the process when the dimensions encoded in the header claim a file is enormous. image-size reads only the header bytes needed to determine dimensions, so it is not vulnerable to decompression bombs (it never decompresses pixel data). However, a crafted PNG header claiming dimensions of 2,147,483,647 × 2,147,483,647 will cause image-size to return those values faithfully — your validation layer must check that width and height are within reasonable bounds before trusting them.
probe-image-size applies additional validation, rejecting images whose claimed dimensions exceed 65,536 pixels in either direction by default. This is a meaningful security control for services that accept remote image URLs from user input — an attacker could supply a URL to a file with absurd header-claimed dimensions, and without bounds checking, downstream code that multiplies width × height to allocate a buffer would crash or allocate gigabytes. Always apply a reasonable dimension ceiling (for example, rejecting images claimed to exceed 10,000 pixels in either dimension) regardless of which library you use. sharp's metadata parser inherits libvips's bounds checking, which has been hardened over many years of processing untrusted images in production environments.
EXIF Privacy and Data Scrubbing
The EXIF metadata that sharp's .metadata() exposes contains fields that carry significant privacy implications. GPS coordinates embedded in photos taken on modern smartphones pinpoint the location where the photo was taken — publishing a user-uploaded image to a public URL without stripping GPS EXIF data leaks the user's location. Camera make and model, capture date and time, and in some cases the device serial number are also embedded. For any application that accepts image uploads from users and serves them publicly, EXIF stripping is a security and privacy requirement, not an optional enhancement.
sharp provides EXIF stripping as part of its re-processing pipeline: sharp(input).withMetadata(false).toFile(output) strips all metadata during the re-encoding step. The tradeoff is that re-encoding (even at high quality settings) introduces minor quality loss for JPEG images. For PNG images, re-encoding is lossless but EXIF stripping in lossless formats requires careful handling to avoid accidentally embedding new metadata. image-size and probe-image-size read metadata but do not modify it — if you're using them for dimension checking as part of an upload pipeline, you still need sharp in the pipeline for the stripping step, making the "use sharp metadata when you're already using sharp" guidance especially relevant for upload processing applications.
Community Adoption in 2026
image-size reaches approximately 15 million weekly downloads, making it one of the most widely used image utility packages in the Node.js ecosystem. This download volume is driven by its inclusion across documentation generators (VitePress, Docusaurus), static site generators, image optimization tooling, and upload validation middleware. The package reads minimal bytes — for a PNG, it needs only 24 bytes to determine dimensions — making it effectively free to run in server-side contexts. The synchronous API is particularly valued in build scripts where async overhead is unnecessary. image-size's broad format support (including AVIF, HEIF, and WebP) and pure-JavaScript implementation (no native binaries to compile) make it the frictionless default for CI/CD pipelines.
probe-image-size at approximately 3 million weekly downloads serves a more specific niche: applications that need image dimensions without downloading the full image content. Link preview generators, SEO validators, CMS media browsers, and Open Graph image verification tools are the primary use cases. The ability to pass a URL directly — and have probe-image-size handle the HTTP request, read only the minimum bytes, and close the connection — makes it particularly efficient in serverless environments where bandwidth and execution time are billed. The package handles the edge cases of streaming image formats where dimensions appear late in the file (some progressive JPEGs) by downloading slightly more data as needed.
sharp at approximately 10 million weekly downloads is the Swiss Army knife of Node.js image processing, and its .metadata() method is used far beyond cases where image-size or probe would suffice. When a pipeline is already resizing, converting, or compressing images with sharp, calling .metadata() adds no meaningful overhead — libvips has already loaded the image. EXIF data extraction (camera make/model, GPS coordinates, capture date), ICC profile inspection, and channel statistics (dominant color for placeholder generation) are capabilities that image-size and probe cannot provide. The native binary requirement is the main friction point — sharp's installation involves downloading a pre-built libvips binary, which occasionally causes issues in constrained CI environments or unusual architectures (Alpine Linux containers with musl libc require special handling).
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on image-size v1.x, probe-image-size v7.x, and sharp v0.33.x.
Compare image processing and media tooling on PkgPulse →
See also: ipx vs @vercel/og vs satori 2026 and cac vs meow vs arg 2026, acorn vs @babel/parser vs espree.