[R3F] Vertical sync with useFrame?

Is there a “built-in” way to conditionally limit framerate with useFrame?

The goal is to add a Vsync option in settings.

In my project, a single useFrame handler drives the render loop, but it seems to go as fast as possible - looks amazing - but runs a little hot, so I’d like a user option to limit it to ~60FPS, or to sync it with their monitor’s refresh rate. I can do it in the useFrame handler, but just wondered if there is a way to do it with renderPriority or something.

renderPriority: Seems to either get called too often (@ ~150+ FPS) or not at all if I set any value.

I may be mistaken since I’m no R3F expert by any means, but I would assume the render loop is internally invoked using requestAnimationFrame, which means the renderer should never exceed the monitor refresh rate. Otherwise React’s UI updates would be misaligned with the browser paint updates, which would be silly imho :smile:

Do you maybe have a monitor that supports higher refresh rates? You can easily test this in any vanilla Three example and check what the FPS counter says.

useFrame subscribes you to a central raf loop. imo you shouldn’t worry, it will always run at monitor refresh rate. if this is just about stuff moving too fast/slow depending on monitor refresh rate you should use deltas, not fixed numbers:

useFrame((state, delta) => {
  ref.current.position.x += 0.1 // ❌
  ref.current.position.x += delta // ✅

otherwise there’s a function called advance, with that you can advance the frameloop yourself with whatever strategy you want.

function FrameLimiter({ fps = 60 }) {
  const { advance, set, frameloop: initFrameloop } = useThree()
  useLayoutEffect(() => {
    let elapsed = 0
    let then = 0
    let raf = null
    const interval = 1000 / fps
    function tick(t) {
      raf = requestAnimationFrame(tick)
      elapsed = t - then
      if (elapsed > interval) {
        advance()
        then = t - (elapsed % interval)
      }
    }
    // Set frameloop to never, it will shut down the default render loop
    set({ frameloop: 'never' })
    // Kick off custom render loop
    raf = requestAnimationFrame(tick)
    // Restore initial setting
    return () => {
      cancelAnimationFrame(raf)
      set({ frameloop: initFrameloop })
    }
  }, [fps])
}
4 Likes

Yep that is the issue - it tries to loop as fast as the monitor refresh rate. Cases like when a player has a 144Hz or 240Hz refresh rate screen, or when they are running lower performance settings I would want to limit it to 60 FPS by default. But give them the option to “[✓] Enable Vertical Sync” in Settings with a little warning if they want.

When it’s not enabled, a text input labeled “Limit FPS:” will be active, with a default value of 60, and that can be the default state of the setting.

advance - this will work fine, thanks for hookifying and giving me the whole thing!