最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Three.js Scene Freezes When Browser Tab is Idle - Stack Overflow

programmeradmin4浏览0评论

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()
        })
    }
}

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()
        })
    }
}
Share Improve this question asked yesterday cconsta1cconsta1 8351 gold badge9 silver badges23 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 1

As noted in the documentation:

requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s, in order to improve performance and battery life.

So, that's why the animation stops at first.

Then there is a possible issue with deltaTime. After a few minutes of idling, deltaTime becomes that few minutes. requestAnimationFrame() will resume after returning the tab, mixer.update() will receive 3-5 minutes of delta and that may break animations. Visibility API may help you handle deltaTime:

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

        // Setup
        this.start = Date.now();
        this.current = this.start;
        this.elapsed = 0;
        this.delta = 0.016; // Typical frame time (in seconds)

        // For tracking the requestAnimationFrame id
        this.animationId = null;

        // Bind methods to preserve context
        this.tick = this.tick.bind(this);
        this.handleVisibilityChange = this.handleVisibilityChange.bind(this);

        // Listen for visibility changes
        document.addEventListener("visibilitychange", this.handleVisibilityChange);

        // Start ticking only if the document is visible
        if (!document.hidden) {
            this.startTicking();
        }
    }

    startTicking() {
        if (!this.animationId) {
            this.animationId = window.requestAnimationFrame(this.tick);
        }
    }

    stopTicking() {
        if (this.animationId) {
            window.cancelAnimationFrame(this.animationId);
            this.animationId = null;
        }
    }

    handleVisibilityChange() {
        if (document.hidden) {
            this.stopTicking();
        } else {
            // Reset the current time to avoid a huge delta when resuming.
            this.current = Date.now();
            this.startTicking();
        }
    }

    tick() {
        const currentTime = Date.now();
        // Calculate delta in seconds
        this.delta = (currentTime - this.current) / 1000;
        this.current = currentTime;
        this.elapsed = (this.current - this.start) / 1000;

        this.trigger('tick');

        // Continue the loop if not paused
        this.animationId = window.requestAnimationFrame(this.tick);
    }
}

Or something like that

Thanks for the reply. It turns out that the problem was not related to the Time.js class or the other classes' update methods. I'm working on 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.

发布评论

评论列表(0)

  1. 暂无评论