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

React中Matter.js组件Props更新后性能衰减问题求助

问题分析与解决方案

核心问题

  1. 资源清理不彻底:每次coins更新都新建Matter引擎、渲染器,但afterUpdate事件监听未移除,且Runner实例未正确停止(原代码用Runner.stop(engine)是错误的,Runner是独立实例),导致旧的事件循环和资源残留。
  2. 图片资源重复创建:afterUpdate回调里每次都new Image(),没有缓存,大量图片对象堆积占用内存。
  3. 冗余渲染器:你自己用Canvas绘制气泡,却同时启用了Matter自带的Render,双重渲染额外消耗性能。
  4. 未复用核心实例:每次props更新都重建整个物理环境,完全没必要,应该复用引擎、世界、Canvas,仅更新物理体。

解决方案:复用核心资源,仅更新物理体

用useRef持久化引擎、世界、Runner、Canvas等实例,组件挂载时仅初始化一次;coins变化时,只替换物理体,同时缓存图片避免重复创建。

修改后的完整代码

import { useState, useEffect, useRef } from 'react';
import { Engine, Runner, World, Bodies, Composite, Events } from 'matter-js';

const Bubbles = ({ coins }) => {
  // 持久化核心实例,避免每次渲染重建
  const engineRef = useRef(null);
  const runnerRef = useRef(null);
  const canvasRef = useRef(null);
  const ctxRef = useRef(null);
  const bubblesRef = useRef([]);
  // 图片缓存,避免重复创建Image对象
  const imageCacheRef = useRef({});

  // 组件挂载时初始化一次核心资源
  useEffect(() => {
    const winwidth = window.innerWidth;
    const winheight = window.innerHeight - 80;

    // 创建引擎和世界
    const engine = Engine.create();
    const world = engine.world;
    engineRef.current = engine;

    // 创建Runner(单独保存实例,方便后续停止)
    const runner = Runner.create();
    Runner.run(runner, engine);
    runnerRef.current = runner;

    // 创建Canvas并挂载到DOM
    const canvas = document.createElement('canvas');
    canvas.width = winwidth;
    canvas.height = winheight;
    document.querySelector('.bubbles').appendChild(canvas);
    canvasRef.current = canvas;
    ctxRef.current = canvas.getContext('2d');

    // 监听引擎更新,自定义渲染
    const afterUpdateHandler = () => {
      const ctx = ctxRef.current;
      if (!ctx) return;

      ctx.clearRect(0, 0, canvas.width, canvas.height);
      bubblesRef.current.forEach(({ bubble, coin }) => {
        const pos = bubble.position;
        const size = coin.size;
        const text = coin.symbol;
        const priceChange = coin.price_change_percentage_24h.toFixed(1);
        
        // 从缓存取图片,没有则创建并缓存
        let image = imageCacheRef.current[coin.image];
        if (!image) {
          image = new Image();
          image.src = coin.image;
          imageCacheRef.current[coin.image] = image;
        }

        // 设置填充和描边样式
        if (priceChange > 0) {
          ctx.fillStyle = 'rgba(5, 222, 5, 0.15)';
          ctx.strokeStyle = 'rgba(5, 222, 5, 0.6)';
        } else if (priceChange < 0) {
          ctx.fillStyle = 'rgba(166, 53, 53, 0.15)';
          ctx.strokeStyle = 'rgba(166, 53, 53, 0.8)';
        } else {
          ctx.fillStyle = 'rgba(255, 255, 89, 0.069)';
          ctx.strokeStyle = 'rgba(187, 231, 11, 0.443)';
        }

        // 绘制气泡背景
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, size / 2, 0, 2 * Math.PI);
        ctx.fill();
        ctx.stroke();

        // 绘制图片(确保图片加载完成)
        if (image.complete) {
          ctx.drawImage(
            image,
            pos.x - size / 5,
            pos.y - size / 2.5,
            size / 2.5,
            size / 2.5
          );
        }

        // 绘制文本
        ctx.font = `${size / 6}px Acme`;
        ctx.fillStyle = 'white';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(text, pos.x, pos.y + size / 6.5);

        ctx.font = `${size / 6.5}px Acme`;
        ctx.fillStyle = 'white';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(`${priceChange}%`, pos.x, pos.y + size / 3);
      });
    };
    Events.on(engine, 'afterUpdate', afterUpdateHandler);

    // 组件卸载时彻底清理资源
    return () => {
      // 移除事件监听
      Events.off(engine, 'afterUpdate', afterUpdateHandler);
      // 停止Runner
      Runner.stop(runner);
      // 清空物理世界
      Composite.clear(world);
      // 销毁引擎
      Engine.clear(engine);
      // 移除Canvas
      canvas.remove();
      // 清空图片缓存
      imageCacheRef.current = {};
    };
  }, []); // 空依赖,仅初始化一次

  // 当coins变化时,更新物理体
  useEffect(() => {
    if (!engineRef.current) return;

    const engine = engineRef.current;
    const world = engine.world;
    const physics = {
      frictionAir: 0,
      friction: 0.2,
      frictionStatic: 0.2,
      restitution: 0.1,
      inertia: Infinity,
      density: 0.8,
    };

    // 移除旧的气泡物理体
    Composite.remove(world, bubblesRef.current.map(item => item.bubble));
    
    // 创建新的气泡物理体
    const newBubbles = coins.map(coin => {
      const x = Math.random() * window.innerWidth;
      const y = Math.random() * window.innerHeight;
      const bubble = Bodies.circle(x, y, coin.size / 2, {
        ...physics,
        render: { fillStyle: 'transparent' },
      });
      Composite.add(world, bubble);
      return { bubble, coin };
    });

    bubblesRef.current = newBubbles;
  }, [coins]); // 仅在coins变化时更新

  return <></>;
};

export default Bubbles;

关键改动说明

  1. 用useRef持久化实例:引擎、Runner、Canvas、上下文、气泡数组、图片缓存都通过useRef保存,组件生命周期内只创建一次。
  2. 分离初始化与更新逻辑:初始化逻辑放在空依赖的useEffect,仅执行一次;coins变化时只更新物理体,不重建整个环境。
  3. 图片缓存:用imageCacheRef缓存已创建的Image对象,避免每次渲染都新建,减少内存占用。
  4. 正确清理资源:移除afterUpdate事件监听,停止Runner实例(原代码错误地停止engine),清空物理世界,销毁引擎,移除Canvas。
  5. 移除冗余的Matter Render:你自己用Canvas实现了渲染,Matter自带的Render完全多余,直接删除减少性能消耗。

内容的提问来源于stack exchange,提问作者FakeHuman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 16:35:23