npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@r01al/use-image-resize

v0.1.0

Published

Resize images in a reusable Web Worker pool with a type-safe React hook.

Readme

useImageResize

useImageResize resizes JPEG, PNG, and WebP files in a reusable Web Worker pool. Decoding, canvas drawing, and encoding stay outside React and off the main thread.

Installation

npm install @r01al/use-image-resize

React 18 or newer is required as a peer dependency.

Quick start

import { useImageResize } from '@r01al/use-image-resize';

function ImageUploader() {
	const { process, results, isProcessing, error, reset } = useImageResize({
		maxWidth: 1600,
		maxHeight: 1600,
		quality: 0.8,
		format: 'image/webp',
		concurrency: 'auto',
	});

	const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
		const files = Array.from(event.target.files ?? []);

		try {
			await process(files);
		} catch (error) {
			console.error('At least one image failed', error);
		}
	};

	const upload = async () => {
		const formData = new FormData();

		for (const result of results) {
			formData.append('images', result.file);
		}

		await fetch('/api/upload', {
			method: 'POST',
			body: formData,
		});
	};

	return (
		<>
			<input
				type="file"
				accept="image/jpeg,image/png,image/webp"
				multiple
				onChange={(event) => void handleChange(event)}
			/>

			{results.map((image) => (
				<img key={image.id} src={image.previewUrl} width={200} alt="" />
			))}

			<button disabled={isProcessing || results.length === 0} onClick={upload}>
				Upload
			</button>
			<button onClick={reset}>Reset</button>

			{error && <div>{error.message}</div>}
		</>
	);
}

In a Next.js application, call the hook from a Client Component.

API

const {
	process,
	results,
	isProcessing,
	error,
	reset,
} = useImageResize(options);

Options

interface UseImageResizeOptions {
	maxWidth?: number;
	maxHeight?: number;
	quality?: number;
	format?: 'image/jpeg' | 'image/png' | 'image/webp';
	concurrency?: 'auto' | number;
	allowUpscale?: boolean;
}

Defaults:

{
	maxWidth: 1920,
	maxHeight: 1920,
	quality: 0.8,
	format: 'image/webp',
	concurrency: 'auto',
	allowUpscale: false,
}

Automatic concurrency uses half of the available logical cores, with a fallback of four logical cores, and clamps the pool size between one and four workers. Explicit positive integers are used as provided.

Images keep their aspect ratio. An image is never enlarged unless allowUpscale is true.

Processing files

The overloads preserve the input shape:

const result = await process(file);          // ImageResizeResult
const results = await process([one, two]);   // ImageResizeResult[]

An optional AbortSignal cancels the current request:

const controller = new AbortController();
const pending = process(files, { signal: controller.signal });

controller.abort();
await pending;

Starting a new batch replaces the hook's previous results and cancels an older batch that is still running. All files enter the pool queue; creating many promises does not create one worker per image.

If one file in a batch fails, the returned promise rejects with an ImageResizeError, while successfully processed files remain available in results.

Results

interface ImageResizeResult {
	id: string;
	originalFile: File;
	file: File;
	blob: Blob;
	previewUrl: string;
	originalWidth: number;
	originalHeight: number;
	width: number;
	height: number;
	originalSize: number;
	outputSize: number;
	sizeRatio: number;
	status: 'done';
}

sizeRatio is outputSize / originalSize. A value below 1 means the output is smaller; a value above 1 accurately represents an output that grew. This is less ambiguous than calling the value a compression ratio.

The output File keeps the original base name and uses an extension matching the selected output format. For example, holiday.jpg becomes holiday.webp.

Preview lifetime and reset

Preview URLs use URL.createObjectURL, not Base64. The hook owns those URLs and revokes them when a new batch replaces the results, reset() is called, or the component unmounts.

reset() also cancels running and queued work, clears results, and resets the public error and processing state.

Errors

Errors are instances of ImageResizeError and include one of these codes:

type ImageResizeErrorCode =
	| 'UNSUPPORTED_FILE'
	| 'DECODE_FAILED'
	| 'CANVAS_UNAVAILABLE'
	| 'ENCODE_FAILED'
	| 'WORKER_FAILED'
	| 'INVALID_OPTIONS'
	| 'BROWSER_UNSUPPORTED'
	| 'CANCELLED';

Architecture

useImageResize
  -> processImageBatch
    -> ImageProcessor
      -> ProcessingBackend
        -> WorkerBackend
          -> WorkerPool
            -> Blob worker (vanilla JavaScript string)
              -> createImageBitmap
              -> calculateResizeDimensions
              -> OffscreenCanvas
              -> Blob

The worker is self-contained vanilla JavaScript stored in a Blob and started through an object URL. There is no separate worker script asset to copy or configure in the consuming bundler. Its object URL is revoked when the backend is destroyed.

The worker pool keeps a fixed fleet alive, assigns queued jobs to idle workers, matches typed responses by job ID, replaces crashed workers, and rejects all unresolved jobs during destruction. ImageProcessor owns file validation, output File creation, and preview creation. React only owns state and resource lifetime.

The ProcessingBackend interface leaves a clean extension point for a future main-thread fallback without coupling it to React or the worker queue.

Browser compatibility

Version 1 intentionally has no main-thread fallback. Processing requires:

  • Web Workers created from Blob object URLs
  • createImageBitmap inside workers
  • OffscreenCanvas with a 2D context and convertToBlob
  • File, Blob, and object URL APIs

The package is safe to import during SSR because browser APIs are not accessed at module initialization. Actual processing must run in a supported browser. Applications with a strict Content Security Policy must allow Blob workers, typically through an appropriate worker-src blob: directive.

Development

npm install
npm run typecheck
npm test
npm run build

npm pack --dry-run runs all checks and shows the files that would be published.

License

MIT