DevInTheMiddle_

Frames per Second in JavaScript

Published on Sep 21, 2020

Every video game you have ever played has - at its core - a loop cycle that listens for user input, processes it updating the current state, and then draws the result on the screen accordingly.

while(true) {
    input()
    update()
    draw()
}

If we measure the time taken for every single game loop cycle in terms of real time passed, we get frames per second (aka frame rate, aka fps).

let fps = 0
let elapsed = 0
let current = performance.now()
let last = performance.now()

setInterval(() => {
    current = performance.now()
    elapsed = current - last
    last = current
    fps = 1000 / elapsed
    input()
    update()
    draw()
})

The problem with this approach is that the duration of a single cycle depends on how much fast the CPU is and how much operations are performed at any given time before processing the next state.

Javascript is single-threaded language, so during the execution of the function called by setInterval no other functions will be executed and the next cycle has to wait for the previous one to be done.

Understandably, the real challenge in game development is to make the game run on a consistent speed on every machine.

Fixed speed

The simplest approach to solve this problem is to run the game at a fixed and consistent speed, so that every frame takes the same amount of time.

In the following scenario we will run our code at the frame rate set by the browser via window.requestAnimationFrame. The number of callbacks is usually 60 frames per second, but will generally match the display refresh rate in most web browsers. The desktop monitor standard is a 60Hz refresh rate.

let fps = 0
let elapsed = 0
let current = performance.now()
let last = performance.now()

function tick() {
    window.requestAnimationFrame(() => {
        current = performance.now()
        elapsed = current - last
        last = current
        fps = 1000 / elapsed
        input()
        update()
        draw()
        tick()
    })
}
tick()

The limit with this approach is that we need to ensure that the logic inside every cycle will not take more time than the frame duration, as the game will slow down.

Real time

To overcome the limitation with the method above we can calculate how much time is passed on every cycle.

Passing the time elapsed between two frames to the update() function allows us to compute the next state based on a real difference in time between two frames.

If a frame takes longer to be computed, our function will be able to calculate the next state taking this delay into account.

let fps = 0
let current = performance.now()
let last = performance.now()
let elapsed = 0

function tick() {
    window.requestAnimationFrame(() => {
        current = performance.now()
        elapsed = current - last
        last = current
        fps = 1000 / elapsed
        input()
        update(elapsed)
        draw()
        tick()
    })
}
tick()

The limitation with this approach is better understable with a real-case scenario. Let's figure out two objects that are going to collide.

If the frame before the collision takes too much time to be computed, on the next frame the objects will be drawn in a new position based solely on the time passed between the frames. The frame in which the collision should have been detected has been skipped completely.

Decoupling

What we can do to fix the issue above is to decouple the update() logic from the draw() logic.

In this way the screen will be re-painted only when all the frames that are lagging behind - if any - have been computed and the game state is the closest to the expected "real time" conditions.

On a fast machine this process will be unnoticeable, on a slow one we will have a choppy rendering, but we will be sure that what is on the screen will reflect the latest state that could have been possibly fully computed.

let lag = 0
let elapsed = 0
let current = performance.now()
let last = performance.now()
let update_rate = 1000 / 60

function tick() {
    window.requestAnimationFrame(() => {
        current = performance.now()
        elapsed = current - last
        last = current
        lag += elapsed
        fps = 1000 / elapsed
        while (lag >= update_rate) {
            update()
            lag -= update_rate
        }
        draw(lag / update_rate)
        tick()
    })
}
tick()

Given that lag can be greather than zero at draw() time - we can pass lag / update_rate to it, in order to take into account small delays between frames, providing a smoother experience.

This small delays will be always under the update_rate duration - so we can ignore the fact that an object could be drawn in a position different from the expected (e.g. due to collisions or changes in speed).

At this point in the logic we can assume to be at the most recent game state update and this remaining delta is so small that any discrepancies will be unnoticeable and immediately fixed at next loop cycle.

Written by

Simone Manzi

GitHub

Senior Software Engineer with strong expertise in Web Development, now writing bugs as Data Engineer.
Thinks of himself to be a real Full-Stack... Only until his impostor syndrome does not take over!