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.
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
| Install | ✓ · 0.5s | 3 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 0.7 KB | gzipped (1.3 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 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
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
- One requestAnimationFrame callback covers the job; five global queues add machinery without improving that case
- You are starting with Motion or Framer Motion today; their public schedulers and animation APIs are the supported integration points
- The work runs on a server: framesync is built around a browser frame source rather than Node.js timers or jobs
- You need priorities, deadlines, idle callbacks, promises, or exception isolation; the API supplies none of those policies
- You require an actively released standalone dependency: 6.1.2 dates to August 2022 and the Popmotion repository was last pushed in March 2024
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
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.

