You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js中使用PIXI.js出现双画布问题求助

Next.js中使用PIXI.js出现双画布问题求助

大家好,我在Next.js项目里用PIXI.js开发游戏的时候遇到了个头疼的问题——页面上居然渲染出了两个canvas,而不是预期的一个。这导致了一些渲染和功能上的异常,我检查过代码,确认没有重复调用创建画布或者添加到DOM的逻辑,但问题还是存在。

我的代码(index.js)

import { useEffect } from 'react';

import * as PIXI from 'pixi.js';

import { Howl } from 'howler';

import { createCharacter, handleCharacterClick } from '../utils/character.js';

import { createScoreCounter, incrementScore, getScore, getScoreCounter } from '../utils/score.js';

import { createBonusCounter, incrementBonusScore, decayBonusScore } from '../utils/bonus.js';

export default function Home() {

useEffect(() => {

let app;

function resize() {

const aspectRatio = 719 / 420; // Aspect ratio of the background image

let w, h;

if (window.innerWidth / window.innerHeight > aspectRatio) {

w = window.innerHeight * aspectRatio;

h = window.innerHeight;

} else {

w = window.innerWidth;

h = window.innerWidth / aspectRatio;

}

app.view.style.width = w + 'px';

app.view.style.height = h + 'px';

app.view.style.left = (window.innerWidth - w) / 2 + 'px'; // Center horizontally

app.view.style.top = (window.innerHeight - h) / 2 + 'px'; // Center vertically

}

// Load the background image and get its dimensions

let backgroundImageOnLoad = new Promise((resolve, reject) => {

const img = new window.Image();

img.onload = () => resolve({ width: img.width, height: img.height });

img.onerror = reject;

img.src = '/assets/images/bed.jpg';

});

backgroundImageOnLoad.then(dimensions => {

// create a new Pixi application with the same dimensions as the background image

app = new PIXI.Application({

width: dimensions.width,

height: dimensions.height

});

// call resize function to properly size the view initially

resize();

// add the app view (canvas element) to the document body

document.getElementById('game').appendChild(app.view);

// create a texture from the background image and add it to the stage

let backgroundTexture = PIXI.Texture.from('/assets/images/bed.jpg');

let backgroundSprite = new PIXI.Sprite(backgroundTexture);

app.stage.addChild(backgroundSprite);

// create character and add to stage

let character = createCharacter(app);

character.interactive = true;

character.on('pointerdown', () => {

handleCharacterClick(character, app);

incrementScore();

});

app.stage.addChild(character);

// create and add score counter to stage

createScoreCounter();

let scoreCounter = getScoreCounter();

app.stage.addChild(scoreCounter);

// create and add bonus counter to stage

const bonusCounter = createBonusCounter();

app.stage.addChild(bonusCounter.container);

// Load the background music

const backgroundMusic = new Howl({

src: ['/assets/audio/background-music-2.mp3'],

loop: true,

volume: 0.5,

});

// Play the background music

backgroundMusic.play();

// attach resize function to window resize event

window.addEventListener('resize', resize);

});

// cleanup logic if needed

return () => {

window.removeEventListener('resize', resize);

};

}, []);

return (

<div id="game"></div>

);

}

渲染后的HTML片段

<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="next-head-count" content="2">
<noscript data-n-css=""></noscript>
<script defer="" nomodule="" src="/_next/static/chunks/polyfills.js?ts=1688999013977"></script><script src="/_next/static/chunks/webpack.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/main.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/pages/_app.js?ts=1688999013977" defer=""></script><script src="/_next/static/chunks/pages/index.js?ts=1688999013977" defer=""></script><script src="/_next/static/development/_buildManifest.js?ts=1688999013977" defer=""></script><script src="/_next/static/development/_ssgManifest.js?ts=1688999013977" defer=""></script>
<style>
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZVJvb3QiOiIifQ== */
</style>
<noscript id="__next_css__DO_NOT_USE__"></noscript>
</head>
<body>
<div id="__next">
<div id="game">
<canvas style="touch-action: none; width: 883.343px; height: 516px; left: 518.329px; top: 0px; cursor: inherit;" width="719" height="420"></canvas>
<canvas style="touch-action: none; width: 883.343px; height: 516px; left: 518.329px; top: 0px; cursor: inherit;" width="719" height="420"></canvas>
</div>
</div>
<script src="/_next/static/chunks/react-refresh.js?ts=1688999013977"></script><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","nextExport":true,"autoExport":true,"isFallback":false,"scriptLoader":[]}</script>
<div id="__next-build-watcher" style="position: fixed; bottom: 10px; right: 20px; width: 0px; height: 0px; z-index: 99999;" data-dashlane-shadowhost="true" data-dashlane-observed="true"></div>
<next-route-announcer>
<p aria-live="assertive" id="__next-route-announcer__" role="alert" style="border: 0px none; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; top: 0px; width: 1px; white-space: nowrap; overflow-wrap: normal;"></p>
</next-route-announcer>
</body>
</html>

我尝试过的排查方向和可能的解决思路

我已经确认主文件里没有重复创建PIXI.Application或者重复调用appendChild的逻辑,但还是出现了双画布。结合Next.js的特性,我猜测可能是以下几个原因:

  • Next.js开发模式的React Strict Mode:开发模式下Strict Mode会刻意重复调用useEffect这类生命周期函数,这可能导致PIXI.Application被创建两次,进而生成两个canvas。

    解决办法:可以先暂时关闭Strict Mode试试——找到_app.js文件,去掉<React.StrictMode>的包裹,重启开发服务看问题是否消失。如果有效,就给useEffect加个初始化标记,确保只执行一次:

    export default function Home() {
      const appInitialized = useRef(false);
    
      useEffect(() => {
        if (appInitialized.current) return;
        appInitialized.current = true;
    
        // 原有代码...
    
        return () => {
          window.removeEventListener('resize', resize);
          app?.destroy(true); // 清理PIXI实例
        };
      }, []);
    
      return <div id="game"></div>;
    }
    
  • PIXI.js的自动附加行为:部分版本的PIXI.js在创建Application时,默认会自动把canvas附加到document.body,而我们又手动添加到了#game元素里,导致两次添加。

    解决办法:创建PIXI.Application时加上autoAttach: false选项,手动控制DOM添加:

    app = new PIXI.Application({
      width: dimensions.width,
      height: dimensions.height,
      autoAttach: false
    });
    
  • 工具函数的意外创建:虽然主文件没问题,但createCharacter、createScoreCounter这些工具函数里会不会有额外创建画布的逻辑?建议检查这些文件,确保没有重复生成canvas的代码。

希望有朋友能帮我定位下问题,或者分享类似问题的解决经验!

备注:内容来源于stack exchange,提问作者Martin Erlic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 07:18:16