Case study · Open source

react-inp-chunker

An open-source, headless React and TypeScript library that keeps heavy synchronous work from blocking user input. It processes work in time slices of a few milliseconds and yields to the main thread between them, so long-running updates do not cost Interaction to Next Paint (INP).

npm install react-inp-chunker

Project proof

My role

  • Designed the API, the scheduling engine, and the state model, and wrote the library in strict TypeScript.
  • Wrote the documentation and the live playground, which runs the same work with and without chunking.
  • Published and maintain the package on npm under the MIT license.

Constraints

  • The library could not make the work faster, only make sure it never holds the main thread for more than a few milliseconds at a time.
  • It had to stay headless and work in React 18 and 19, under server rendering, and outside React.
  • Results had to stay consistent under concurrent React, with nothing running after cancel or unmount.

Key decisions

  • Split a React-free scheduling core from the hooks so the same engine runs in plain TypeScript, a Web Worker, or Node.
  • Read the clock after every item to bound each slice overrun to one item, and added batchSize for work too cheap to time item by item.
  • Chose the yield mechanism per browser: scheduler.yield(), then MessageChannel, then setTimeout.
Project type
Open-source React library on npm
Role
Author and maintainer
Primary focus
INP, main-thread scheduling, DX
Stack area
TypeScript, React 18+, Scheduler API
  • Open source
  • React
  • TypeScript
  • INP
  • Core Web Vitals
  • npm package

Context

One thread runs a page’s JavaScript, handles every click and key press, and paints the screen. Filtering thousands of rows, parsing a large CSV, or building a search index inside a React component holds that thread, and any input that arrives meanwhile waits in a queue.

A task longer than 50 ms is a long task, and an interaction that lands in one is the most common way to fail Interaction to Next Paint (INP), the Core Web Vital for responsiveness.

Problem

The work itself is often necessary and cannot simply be made faster. The goal was a small, typed primitive that makes sure the work never holds the main thread for more than a few milliseconds at a time, without asking teams to restructure their components.

  • A long synchronous loop blocks clicks, typing, and painting until it returns.
  • Yielding by hand with nested setTimeout calls is clamped to a minimum delay of about 4 ms and spreads scheduling logic through components.
  • Progress, pause, resume, and cancel have to reach React state without tearing under concurrent rendering.
  • No state update may arrive after a component unmounts.

Approach

The library processes items back to back until the time slice (5 ms by default) is used up, or until the browser reports pending input. It then yields so the browser can handle input and paint, and resumes where it stopped.

It yields with the best tool the browser has: scheduler.yield() where it is supported, MessageChannel in Safari and older browsers, and setTimeout(0) as the last resort.

  • useChunkedTask runs one function over a list and re-renders with progress once per slice, not once per item.
  • useDeferredTask runs work written as a generator and pauses at each yield once the slice is used up.
  • react-inp-chunker/core exposes createChunkScheduler and createDeferredTask without importing React, for plain TypeScript, a Web Worker, or a Node script.
  • Both engines expose start, pause, resume, cancel, a subscribe and getSnapshot pair, and a settled promise that always resolves.
  • batchSize and collectResults tune work at the extremes; most code needs neither.

Guarantees and developer feedback

A slice overruns by at most one item, or batchSize items if that option is raised, because the clock is read after every item. A settled task never reports its results a render late, nothing runs after cancel(), and no state update arrives after unmount.

In development, a single item that blocks for more than 50 ms is reported with its index, and work cheap enough that the clock read dominates gets a batchSize suggestion. Both warnings are compiled out of the production build that bundlers select through the production export condition.

Compatibility, quality, and footprint

The package supports React 18 and 19, Node 18, 20, 22, and 24, ESM and CommonJS, and server rendering, where both hooks render an idle state and run nothing. The test suite runs on both React majors at 100% statement, branch, function, and line coverage.

It has no dependencies beyond React. The README reports 1.83 kB for useChunkedTask, 1.72 kB for useDeferredTask, 1.34 kB for createChunkScheduler without React, and 2.35 kB for everything, minified and compressed with Brotli. Releases are published from CI through npm Trusted Publishing with signed provenance.

Outcomes

  • Published on npm as react-inp-chunker under the MIT license, with documentation and a live playground on GitHub Pages.
  • Provides two React hooks, useChunkedTask and useDeferredTask, and a React-free core that runs the same scheduling engine.
  • Keeps each slice of work to a few milliseconds by default, so input handling and painting can run between slices.
  • Checks its compatibility claims, including React 18 and 19, four Node versions, ESM, and CommonJS, on every push.

Next steps

  • Read the documentation and try the live playground.
  • Install it with npm install react-inp-chunker.
  • Report issues or contribute on GitHub.