Three.js Scene Freezes When Browser Tab is Idle

I’m working on a Three.js project with a Tamagotchi-style animated character using GLTF animations and an animation mixer. The project includes a robot, a hatching box, and interactive controls.

Everything runs smoothly until the browser tab is idle for a few minutes. When I return to the window the scene appears frozen and the robot stops animating.

My animation loop is in the class Experience.js:

this.time.on('tick', () => {
    this.update();
});

update() {
    const deltaTime = this.time.delta;
    this.world.update(deltaTime);
    this.camera.update();
    this.renderer.update();
}

In the class World.js the update is:

update(deltaTime) {
        if (this.box) {
            this.box.update(deltaTime)
        }
        if (this.robot) {
            this.robot.update(deltaTime)
        }
    }

Inside the Box.js:

update(deltaTime) {
        if (this.mixer) {
            this.mixer.update(deltaTime)
        }
    }

In the Robot.js:

update(deltaTime) {
        if (this.tamagotchiController) {
            this.tamagotchiController.update(deltaTime)
        }
    }

Animation updating in TamagotchiController.js:

update(deltaTime) {
    if (this.mixer) {
        this.mixer.update(deltaTime);
    }
}

Finally the Camera.js update is:

update() {
        if (this.controls) {
            this.controls.update()
        }
    }

and the Renderer.js:

update() {
        this.instance.render(this.scene, this.camera.instance)
    }

The live project is here

I have no idea what is going wrong because no errors appear on the console.

Here is the Time.js used in the project:

import EventEmitter from './EventEmitter.js'

export default class Time extends EventEmitter {
    constructor() {
        super()

        // Setup
        this.start = Date.now()
        this.current = this.start
        this.elapsed = 0
        this.delta = 0.016 // Initialize with a typical frame time in seconds

        window.requestAnimationFrame(() => {
            this.tick()
        })
    }

    tick() {
        const currentTime = Date.now()
        this.delta = (currentTime - this.current) / 1000 // Convert milliseconds to seconds
        this.current = currentTime
        this.elapsed = (this.current - this.start) / 1000 // Convert milliseconds to seconds

        this.trigger('tick')

        window.requestAnimationFrame(() => {
            this.tick()
        })
    }
}

The source code is here.

It looks like 99% custom code, so it’s a bit hard to help, but what’s most likely happening is the deltaTime gets too large - accumulating too many ticks of world updates, freezing the tab when trying to execute all these updates at once.

Make sure you’re not accumulating executions unnecessarily when browser window / tab is not focused (docs.)

2 Likes

100%

OP needs to clamp this.delta to something sane… like 1000

also: javascript - performance.now() vs Date.now() - Stack Overflow

also: three.js docs

Or you won’t work in VR/AR or scenarios where threejs needs control over when rendering occurs.

@cconsta1 Ditch Date.now() and use THREE.Clock()
Clamp your deltaT to something that wont kill your app… (like 1000)
And don’t run your renderer off of requestAnimationFrame, use .setAnimationLoop and THREE.Clock()'s .getDelta()

1 Like

Thank you guys for your replies! It turns out the problem was not related to the update methods and the deltaTime becoming too large, though it was great to become aware of this trick.

This code is from a Tamagotchi-style robot character using the Expressive Robot from the Three.js examples and an animation mixer. The robot performs actions like feeding, playing, and cleaning. I wrote methods that create waste objects generated periodically, which the user can clean like the classic Tamagotchi game.

The scene froze after the browser tab was idle because the reset() method (of the class creating the waste) restarted the waste creation loop without clearing the previous interval. This caused multiple intervals to stack, creating excessive waste objects and freezing the scene.

Old code (from reset()):

this.wasteObjects.forEach(waste => this.scene.remove(waste));
this.wasteObjects = [];
this.startWasteCreation(); // Restarted without clearing old interval

The problem was that startWasteCreation() was called without clearing the previous interval. So overlapping intervals created too many waste objects over time, causing performance issues.

New code:

if (this.wasteCreationInterval) {
    clearInterval(this.wasteCreationInterval); // Clear previous interval
}
this.wasteObjects.forEach(waste => this.scene.remove(waste));
this.wasteObjects = [];
this.startWasteCreation(); // Restart safely

Now the scene no longer freezes after the tab is idle :slight_smile:

It took me a while to track this bug down!

1 Like