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

tempus

v1.0.0-dev.18

Published

one rAF to rule them all

Readme

Tempus

TEMPUS

Introduction

tempus means time in Latin, this package is a lightweight, high-performance animation frame manager for JavaScript applications.

Packages

Features

  • One shared rAF loop — merges every requestAnimationFrame call into a single loop to cut per-frame overhead
  • Explicit ordering — run animations in an explicit order each frame instead of registration order
  • Custom frame rates — throttle callbacks to a target FPS (absolute like 30, or relative like '50%') independent of the display refresh
  • Frame budget — every callback gets state.budget() (ms left this frame) to gracefully skip or chunk work
  • Playback controlplay(), pause() and restart() the whole loop at once
  • rAF patchingpatch() absorbs every native requestAnimationFrame (including third-party and minified loops) into the shared loop
  • Live profiler overlaytempus/profiler draws a real-time timeline of how each callback fills the frame budget
  • IntrospectionTempus.inspect() exposes per-callback timing for added and patched loops alike
  • Library-friendly — drop-in compatible with GSAP, Lenis, and other animation tools
  • Zero dependencies — no external packages, nothing extra to audit
  • ~1KB gzipped — a negligible footprint for a core primitive

Installation

using package manager

npm install tempus
import Tempus from 'tempus'

using script tag

<script src="https://unpkg.com/[email protected]/dist/tempus.min.js"></script> 

Basic Usage

import Tempus from "tempus"

// Simple animation at maximum FPS.
// Every callback receives a single `state` object:
// { time, deltaTime, frame, budget }
function animate({ time, deltaTime, frame, budget }) {
  console.log('frame', time, deltaTime)
}

Tempus.add(animate)

Cleanup

const unsubscribe = Tempus.add(animate)

unsubscribe()

Playback Control

Tempus.pause() // no rafs will be called
Tempus.play() // resume
Tempus.restart() // set clock elapsed time to 0

React

See tempus/react

Advanced Usage

Custom Frame Rates

Tempus.add(animate, { 
  fps: 30 // Will run at 30 FPS
})

Tempus.add(animate, { 
  fps: '50%' // Will run at 50% of the system's FPS
})

Order System

order is a sort key for execution within a frame — lower runs first, exactly like CSS order. Default is 0; negative values run before it, positive after.

——[-Infinity]——[0]——[Infinity]——> execution order

Input

// Default order: 0 (runs second)
Tempus.add(() => console.log('animate'))

// Order: 1 (runs third)
Tempus.add(() => console.log('render'), { order: 1 })

// Order: -1 (runs first)
Tempus.add(() => console.log('scroll'), { order: -1 })

Output

scroll
animate
render

Idle Pattern (frame budget)

state.budget() returns the milliseconds left in the current frame before it exceeds the budget (1000 / Tempus.targetFps, default 60fps ≈ 16.67ms). It's the live equivalent of requestIdleCallback's timeRemaining(), so you can gate optional or expensive work and avoid blocking the main thread:

// run only when there's spare frame time left
Tempus.add(({ budget }) => {
  if (budget() > 0) doExpensiveWork()
})

// or chew through work in chunks until the budget runs out.
// budget() is live, so calling it again inside the loop reflects time already spent.
Tempus.add((state) => {
  while (state.budget() > 0) {
    doChunkOfWork()
  }
})

Tune the target with Tempus.targetFps (default 60). Note this is frame-budget idle — leftover time before the frame is over budget — not the browser's true post-paint idle. For genuine background work, prefer native requestIdleCallback.

Ping Pong Technique

ping and pong will alternate between each frame, but never during the same frame

Tempus.add(({ frame }) => {
  if (frame % 2 === 0) {
    console.log('ping')
  } else {
    console.log('pong')
  }
})

Global RAF Patching

// Patch native requestAnimationFrame across all your app
Tempus.patch()
// Now any requestAnimationFrame recursive calls will use Tempus

// Restore the original requestAnimationFrame when you're done
Tempus.unpatch()

Patching absorbs every native requestAnimationFrame call — including loops inside third-party and minified libraries — into the single shared loop, with no name detection or string matching. Re-registering callbacks run on the next frame (matching native one-shot rAF semantics), and a throwing callback is caught and logged so it can't take down the rest of the frame.

Labelling Callbacks

Give a callback a label so it's easy to identify in the profiler overlay and in Tempus.inspect():

Tempus.add(animate, { label: 'hero-parallax' })

Profiler Overlay

tempus/profiler mounts a live, draggable overlay that visualises how a single frame is composed. It lays every callback — both Tempus.add() callbacks and loops absorbed by Tempus.patch() — end-to-end on a timeline whose full width is the per-frame budget (1000 / Tempus.targetFps), so you can watch the frame fill up and overflow in real time.

import { profiler } from 'tempus/profiler'

const overlay = profiler({
  corner: 'top-left', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
  fps: 5,             // overlay refresh rate (the measurements it shows are unaffected)
  container: document.body, // mount target
})

// later
overlay.destroy()

The panel shows live FPS and budget usage in its header, a colour-coded timeline (throttled callbacks are hatched, the over-budget region is highlighted in red), and a per-callback legend with its order, target FPS and average cost. Click the header to collapse it, drag it to reposition, or use the play/pause button to start and stop the whole loop. It's SSR-safe — on the server profiler() returns a no-op handle.

Introspection

Tempus.inspect() returns a normalized timing snapshot of every active callback — added and patched alike — which is what powers the profiler overlay:

Tempus.inspect()
// [{ label, samples, order, fps, source: 'add' | 'patch' }, ...]

Integration Examples

With Lenis Smooth Scroll

// lenis.raf expects a time in ms, so pull it off the state object
Tempus.add(({ time }) => lenis.raf(time))

With GSAP

// Remove GSAP's internal RAF
gsap.ticker.remove(gsap.updateRoot)

// Add to Tempus
Tempus.add(({ time }) => {
  gsap.updateRoot(time / 1000)
})

With Three.js

Tempus.add(() => {
  renderer.render(scene, camera)
}, { order: 1 })
// the render will happen after other rafs
// so it can be synched with lenis for instance

API Reference

Tempus.add(callback, options)

Adds an animation callback to the loop.

  • callback: (state: TempusState) => void, where TempusState is:
    • time: number - Elapsed time in ms since the loop started
    • deltaTime: number - Time in ms since this callback's previous run
    • frame: number - Frame counter
    • budget: () => number - Call it for the ms left in the current frame before exceeding the budget (live)
  • options:
    • order: number (default: 0) - Sort key for execution order; lower runs first (like CSS order)
    • priority: number - Deprecated alias for order
    • fps: number | string (default: Infinity) - Target frame rate. A number throttles to that absolute FPS; a string like '50%' runs at a fraction of the system frame rate
    • label: string - Optional name shown in Tempus.inspect() and the profiler overlay
  • Returns: () => void - Unsubscribe function

Tempus.play()

Starts (or resumes) the loop. The loop auto-starts on the client when Tempus is imported.

Tempus.pause()

Stops the loop; no callbacks run until play() is called.

Tempus.restart()

Resets the clock's elapsed time to 0 and resumes the loop.

Tempus.isPlaying

boolean - Whether the loop is currently running.

Tempus.targetFps

number (default: 60). The frame rate state.budget() is measured against — the budget per frame is 1000 / Tempus.targetFps ms.

Tempus.fps

number - The live measured frame rate (1000 / deltaTime) of the most recent frame.

Tempus.usage

number - Fraction of the last frame's delta spent inside Tempus callbacks (duration / deltaTime).

Tempus.inspect()

Returns a TempusCallbackInfo[] timing snapshot of every active callback (both Tempus.add() callbacks and loops absorbed by patch()):

  • label: string
  • samples: number[] - recent per-frame durations in ms
  • order: number
  • fps: number | string
  • source: 'add' | 'patch'

Tempus.patch()

Patches the native requestAnimationFrame to route every call through Tempus's single loop.

Tempus.unpatch()

Restores the original native requestAnimationFrame and cancelAnimationFrame.

profiler(options)

import { profiler } from 'tempus/profiler' — mounts the live frame-budget overlay and returns a { element, destroy } handle.

  • options:
    • corner: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' (default: 'top-left') - where to pin the panel
    • fps: number (default: 5) - overlay refresh rate
    • container: HTMLElement (default: document.body) - mount target

Best Practices

  • Order callbacks deliberately: things others depend on (like scroll) should run first — give them a lower order (e.g. -1)
  • Clean up animations when they're no longer needed
  • Consider using specific FPS for non-critical animations to improve performance (e.g: collisions)
  • Gate optional or expensive work on state.budget() so it yields when the frame is full
  • Use Ping Pong technique for heavy computations running concurrently

License

MIT © darkroom.engineering

Shoutout

Thank you to Keith Cirkel for having transfered us the npm package name 🙏.