Summary
During development of a Pixi.js-based game using Vite, refreshing the page results in two canvases being appended to the DOM instead of one. This issue does not occur in production builds but causes visual glitches, altered physics, and doubled game speed during development.
Root Cause
The issue stems from Vite’s hot module replacement (HMR) mechanism re-executing the main.js file on each refresh without removing the previously appended canvas. This leads to:
- Multiple canvases being added to the DOM (
container.appendChild(app.canvas)is called repeatedly). - Multiple Pixi.js application instances running concurrently, causing physics and game logic to execute twice as fast.
Why This Happens in Real Systems
- HMR in Vite reloads modules without clearing the existing DOM structure.
- Pixi.js application initialization is not idempotent—each execution creates a new canvas and ticker.
- Lack of cleanup logic in the code to remove or reset the existing application instance.
Real-World Impact
- Visual bugs: Overlapping canvases distort the game’s appearance.
- Physics inconsistencies: Game logic runs at double speed, breaking gameplay mechanics.
- Development inefficiency: Requires manual intervention (e.g., page reload) to reset the state.
Example or Code
// Before fix: Repeated canvas appends on HMR
export let app = new Application();
(async () => {
await app.init({ /* ... */ });
const container = document.getElementById("pixi-container");
container.appendChild(app.canvas); // This runs multiple times on HMR
})();
// Fix: Ensure only one canvas exists
export let app;
if (!document.querySelector("#pixi-container canvas")) {
app = new Application();
(async () => {
await app.init({ /* ... */ });
const container = document.getElementById("pixi-container");
container.appendChild(app.canvas);
})();
}
How Senior Engineers Fix It
- Prevent multiple canvas appends: Check if a canvas already exists before appending a new one.
- Destroy existing Pixi.js instances: Use
app.destroy(true)to clean up resources before reinitializing. - Leverage Vite plugins: Implement a custom HMR handler to reset the application state on updates.
- Encapsulate initialization: Wrap the Pixi.js setup in a function that ensures idempotency.
Why Juniors Miss It
- Lack of understanding of HMR behavior: Juniors often assume module reloads replace the entire DOM.
- Overlooking cleanup: Failing to destroy existing instances before reinitializing.
- Not testing edge cases: Refreshing the page multiple times during development is rarely tested thoroughly.
- Focusing on visual fixes: Clearing the DOM (
container.innerHTML = "") without addressing the root cause.