mrkeyoor.com_
Sat 08 Aug 21:56 UTC
npmWeb Frontendupdated 08 Aug 2026

framesync

framesync is a browser render-loop scheduler from the Popmotion monorepo. It divides each animation frame into five ordered phases: read, update, preRender, render, and postRender. Callbacks in the same phase are deduplicated, recurring callbacks can stay alive across frames, and each receives a shared timestamp and clamped frame delta. Its main purpose is coordinating DOM reads and writes so unrelated code does not cause repeated synchronous layout.

Verdict

framesync remains a neat solution when five shared render phases are already part of the design. For new code, prefer native requestAnimationFrame for a single loop or a currently maintained animation package for a full system.

API stability4/5The 6.1.2 API is deliberately narrow: five methods each on sync, cancelSync, and flushSync, plus getFrameData. The README and TypeScript implementation agree on callback identity, keepAlive, immediate scheduling, and phase order. Six major versions show that this was not always static, but the current release has been unchanged since August 2022 and its fixed shape is easy to isolate behind an application wrapper.
Docs4/5The package README explains the five phases, frame data, recurring work, immediate scheduling, and cancellation with short examples. Source types make the remaining API legible. It does not explain server rendering, exception behavior, phase flushing, delta clamping, or the package's relationship to current Motion packages, and the npm homepage now returns an access error, so discovery is worse than the content itself.
Maintenance2/5The package is not deprecated and its monorepo is not archived, but framesync 6.1.2 was published in August 2022 and the Popmotion repository was last pushed in March 2024. GitHub reports 50 open issues and pull requests across the whole monorepo, not specifically this package. The utility looks stable enough for existing users, yet there is no evidence of recent standalone releases or focused stewardship.
Ecosystem4/5framesync recorded 3,422,880 downloads in the measured week and comes from the Popmotion repository, which has 20,163 stars and powered widely used animation tooling. It includes declarations and dual module formats. Direct community usage is harder to judge because much of that traffic is transitive through animation packages, and integrations generally document their higher-level public APIs rather than framesync itself.

Use it if

  • You are maintaining Popmotion-era animation code that already uses framesync's five render phases
  • Several independent components need one deterministic read-before-write frame loop
  • You want recurring requestAnimationFrame work with cancellation and a shared, clamped delta
  • You need to synchronously flush a named phase in tests or tightly controlled rendering code
Skip it if

Setup reality

npm install framesync provides ESM, CommonJS, and TypeScript declaration entry points plus one runtime dependency on tslib 2.4.0. The default export is the scheduling object; cancelSync, flushSync, and getFrameData are named exports. There is no provider, config file, CSS, or peer dependency. The important setup is architectural: every DOM measurement belongs in read, state calculation in update, and mutations in render or postRender. Mixing reads and writes inside one callback defeats the reason to use the package. Scheduling the same function more than once in a phase is deduplicated by function identity, so do not create a new arrow function if you plan to cancel it later. The second argument keeps a callback alive every frame until the matching cancelSync method receives that exact function. The third, immediate, only appends to the current phase when that phase is already processing; otherwise it behaves like a normal next-frame schedule. FrameData is one object recycled across frames, so destructure delta and timestamp before storing or using them asynchronously. Delta is clamped to 1 through 40 milliseconds after the initial default timestep, which prevents huge animation jumps but means it is not an exact wall-clock measurement. flushSync is a sharp tool: it manually processes a phase and reentrant flushes are deferred, so production code should rarely need it.

Patterns

Measure layout in the read phaseschedule-dom-read

import sync from 'framesync'

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

Keep layout reads together here and postpone style changes until render to avoid forcing layout repeatedly.

Apply a style in the render phaseschedule-dom-write

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

render runs after read, update, and preRender in the same frame, so measurements should already be complete.

Read, calculate, and render in ordercoordinate-frame-phases

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

All callbacks use a global phase order; do not assume scheduling call order can put render before read.

Update from frame delta and timestampuse-frame-data

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

delta is clamped between 1 and 40 milliseconds after the first frame, so it is intended for stable animation rather than precise elapsed-time accounting.

Run an update every framekeep-process-alive

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

sync.update(tick, true)

The true second argument reschedules the same callback after each run until it is explicitly cancelled.

Stop a keep-alive callbackcancel-recurring-process

import sync, { cancelSync } from 'framesync'

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

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

Cancellation requires the same function object and the matching phase; a new arrow function will not cancel the scheduled one.

Append work to the phase currently runningschedule-current-step

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

immediate only means the current frame when update is already processing. Outside that situation it schedules the next frame.

Defer nested work to the following frameschedule-next-frame

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

The default immediate value is false, so work scheduled into its currently running phase waits for the next frame.

Copy frame values before async useavoid-frame-object-retention

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

The FrameData object is reused and mutated every frame; retain primitive values, not the object reference.

Coalesce repeated scheduling by identitydeduplicate-callback

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

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

A phase queues the same callback object once, so this renders the latest value without one callback per sample.

Flush a phase synchronously in a testflush-in-test

import sync, { flushSync } from 'framesync'

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

flushSync bypasses normal frame timing. Reserve it for tests or controlled internals, not routine application rendering.

Read the scheduler's latest frame datainspect-current-frame

import { getFrameData } from 'framesync'

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

getFrameData returns the shared mutable object. Destructure immediately if the values will outlive the current synchronous call.

Alternatives

PackageRegistryPick it when
motionnpmNew animation work that wants the actively developed Motion APIs instead of a low-level Popmotion scheduler
raf-schdnpmYou only need to coalesce repeated calls and run the latest arguments once per animation frame
fastdomnpmYour central problem is batching DOM measures and mutations with a familiar two-queue API