mrkeyoor.com_
Wed 23 Sept 00:32 UTC
npmWeb Frontendupdated 22 Sept 2026

framesync review

Our install confirms that framesync 6.1.2 is a small browser frame coordinator from the Popmotion monorepo. It runs callbacks through five fixed phases named read, update, preRender, render, and postRender, sharing one timestamp and a clamped delta for the frame. Repeated scheduling of the same function in one phase is deduplicated, and a callback can remain active until cancelled. This is useful for keeping DOM measurements ahead of mutations; it is not an animation engine, task queue, or server scheduler.

Verdict

framesync 6.1.2 installed in 0.5 seconds and bundled to 1.3 KB minified in our sandbox, but it has not released since 2022. Keep it for code that already relies on its five phases; new single-loop code is clearer with requestAnimationFrame or a current animation package.

We installed it

Lab card: what happened when we installed framesyncScreenshot of framesync documentation
Install✓ · 0.5s3 packages on disk · 1 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser0.7 KBgzipped (1.3 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does framesync install cleanly?

Yes. In a fresh container with an empty cache, npm install framesync finished in 0.5s, leaving 3 packages and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does framesync add to a browser bundle?

0.7 KB gzipped (1.3 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does framesync work with both ESM and CommonJS?

Yes. Both import 'framesync' and require('framesync') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does framesync include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

framesync or motion: which should you use?

motion: Use it for actively maintained animation and scheduling APIs in new browser work. framesync 6.1.2 installed in 0.5 seconds and bundled to 1.3 KB minified in our sandbox, but it has not released since 2022.

When should you not use framesync?

One requestAnimationFrame callback covers the job; five global queues add machinery without improving that case

API stability4/5The 6.1.2 public surface is limited to five phase methods on sync, cancelSync, and flushSync, plus getFrameData. Its declarations and package documentation agree on callback identity, keepAlive, immediate execution, and phase order. The API has stayed unchanged since August 2022. Six historical majors prevent a perfect score, but this release is small enough to wrap and test without exposing scheduler details throughout an application.
Docs3/5The package documentation names all five phases and demonstrates recurring callbacks, cancellation, frame data, and immediate work. The TypeScript declarations are short enough to inspect. It does not give enough attention to reused FrameData objects, the 1 to 40 millisecond delta clamp, callback exception behavior, synchronous flushing, or how this old package relates to the current Motion stack, leaving operational details in source.
Maintenance2/5framesync 6.1.2 was published in August 2022, while the Popmotion repository's latest push was March 12, 2024. The repository is not archived and the npm package is not marked deprecated, so existing installations have not been formally abandoned. Still, 50 open issues and pull requests sit across the monorepo and there has been no recent release or package-specific activity to show active standalone stewardship.
Ecosystem4/5npm recorded 3,329,256 downloads during August 18 through August 24, 2026, and the Popmotion repository has 20,166 stars. Dual module builds, bundled types, and years of transitive use in animation tooling make the package familiar to bundlers. Much of that traffic arrives through older dependencies, however, while developers starting today are directed toward higher-level Motion packages rather than framesync's direct API.

Use it if

  • Existing Popmotion-era code already depends on framesync's five named phases
  • Several components must share one read-before-write requestAnimationFrame loop
  • A recurring frame callback needs cancellation plus a shared timestamp and clamped delta
  • Tests need to flush a specific render phase synchronously
Skip it if

Setup reality

Our fresh Node 22 sandbox installed framesync 6.1.2 in 0.5 seconds. The result was 3 packages and 1 MB on disk, including 1 direct dependency and no peers. npm audit found 0 known vulnerabilities. The package is 132 KB unpacked, MIT licensed, and includes TypeScript declarations. require() and ESM import both succeeded through its exports map.

There are no credentials, native builds, stylesheets, providers, or config files. The sole declared dependency is tslib 2.4.0. Import the scheduling object as the default export; cancelSync, flushSync, and getFrameData are named exports. Our whole-package browser probe measured 1.3 KB minified and 0.7 KB gzipped.

Put measurements in read, calculations in update, and DOM writes in render or postRender. A callback scheduled twice in one phase is deduplicated by function identity. Cancellation also needs that exact function, so an inline arrow created at cancellation time will not match. Passing keepAlive as the second argument repeats work until the corresponding cancelSync phase receives the original callback.

FrameData is reused rather than replaced, so copy delta and timestamp before retaining them asynchronously. Delta is clamped between 1 and 40 milliseconds after the initial timestep; it is animation input, not precise elapsed time. The immediate flag only joins the phase currently being processed. flushSync can force a phase in a test, while nested flushes are deferred and make production sequencing harder to reason about.

Patterns

Queue a DOM measurement schedule-dom-read

import sync from 'framesync'

sync.read(() => {
  const box = element.getBoundingClientRect()
  latestWidth = box.width
})

read callbacks run before update and render in the same frame, which keeps this measurement ahead of writes.

Place DOM mutation in the render phase schedule-dom-write

sync.render(() => {
  element.style.transform = `translateX(${nextX}px)`
})

render follows read, update, and preRender. Mixing a layout read into this callback can force synchronous layout.

Split one frame across all five queues coordinate-frame-phases

sync.read(() => {
  width = element.getBoundingClientRect().width
})
sync.update(() => {
  nextScale = targetWidth / width
})
sync.render(() => {
  element.style.transform = `scaleX(${nextScale})`
})

The phase order is fixed at read, update, preRender, render, then postRender in version 6.1.2.

Read the shared frame timing values use-frame-data

sync.update(({ delta, timestamp }) => {
  position += velocity * (delta / 1000)
  lastTimestamp = timestamp
})

Each callback receives the current timestamp and a delta clamped to 1 through 40 milliseconds after startup.

Repeat one callback across frames keep-process-alive

const tick = ({ delta }) => {
  position += velocity * (delta / 1000)
}

sync.update(tick, true)

Passing true as keepAlive reschedules the same function until the matching cancelSync method removes it.

Cancel with the original function reference cancel-recurring-process

import sync, { cancelSync } from 'framesync'

const tick = () => updatePhysics()
sync.update(tick, true)

function stop() {
  cancelSync.update(tick)
}

Cancellation compares function identity. A newly created arrow cannot remove the callback that was previously scheduled.

Join a phase that is already running schedule-current-step

sync.update(() => {
  sync.update(() => finalizeState(), false, true)
})

The immediate flag appends only while that same phase is processing; otherwise work waits for its normal turn.

Defer work from one phase to the next frame schedule-next-frame

sync.render(({ timestamp }) => {
  paintFirstState()
  sync.render((next) => {
    console.log(next.timestamp > timestamp)
    paintSecondState()
  })
})

Scheduling without immediate during an active phase places the callback on the following frame rather than recursing.

Copy timing fields before asynchronous use avoid-frame-object-retention

sync.postRender(({ delta, timestamp }) => {
  queueMicrotask(() => {
    sendMetric({ delta, timestamp })
  })
})

getFrameData returns a shared object whose fields change each frame, so retaining the object gives later values.

Coalesce repeated scheduling by identity deduplicate-callback

const renderPosition = () => {
  element.style.transform = `translateX(${latestX}px)`
}

for (const x of pointerSamples) {
  latestX = x
  sync.render(renderPosition)
}

A phase runs one copy of a function even if the same reference is scheduled several times before processing.

Force one phase during a deterministic test flush-in-test

import sync, { flushSync } from 'framesync'

let called = false
sync.update(() => { called = true })
flushSync.update()
expect(called).toBe(true)

flushSync processes the named queue immediately. Nested flush attempts are deferred and should not shape production control flow.

Inspect the scheduler clock without mutation inspect-current-frame

import { getFrameData } from 'framesync'

const { delta, timestamp } = getFrameData()
console.log({ delta, timestamp })

getFrameData exposes timestamp and delta for the current frame; it does not create a new timing snapshot.

Alternatives

PackageRegistryPick it when
motionnpmUse it for actively maintained animation and scheduling APIs in new browser work
raf-schdnpmUse it to collapse many calls into one latest-arguments callback per animation frame
fastdomnpmUse it when two queues for DOM measurement and mutation are enough

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.