首页 > 解决方案 > 为什么我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说是 60?

问题描述

我正在尝试使用 react 开发经典游戏教程how-to-make-a-simple-html5-canvas-game

一切都很顺利,直到我发现我的动作有点小故障,在线测试链接代码

在此处输入图像描述

虽然用 JS 编写的原始游戏要流畅得多:

在此处输入图像描述

所以我深入研究了一下,发现实际的 fps 是不同的:

反应:

在此处输入图像描述

纯JS: 在此处输入图像描述

奇怪的是,在我向 calc fps 添加了一些代码之后,我在 react hook 和 useEffect 中都得到了“60 fps”:


// log interval in useEffect
  useEffect(() => {
    console.log('interval', Date.now() - renderTime.current);
    renderTime.current = Date.now();
  });

// calc fps in hook directly
fps: rangeShrink(Math.round(1000 / (Date.now() - time.current)), 0, 60),

// render
         <Text 
          x={width - 120} 
          y={borderWidth} 
          text={`FPS: ${fps}`}
          fill="white"
          fontSize={24}
          align="right"
          fontFamily="Helvetica"
        />

在此处输入图像描述

定位问题

我添加了一个对比鲜明的画布,每次heroPos更新时都会呈现。它让我在 chrome 开发工具中获得 60FPS。现在问题肯定是由我正在使用的画布库引起的:react-konva。

  const canvasRef = useRef(null);

  useEffect(() => {
    const ctx = canvasRef.current.getContext('2d');
    if (backgroundStatus === 'loaded') {
      ctx.drawImage(backgroundImage, 0, 0);
    }
    if (heroStatus === 'loaded') {
      ctx.drawImage(heroImage, heroPos.x, heroPos.y);
    }
  }, [backgroundStatus, heroStatus, heroPos]);

定位问题

我找到了问题,它是由使用的batchDraw react-konva引起的:

改变这条线后,我现在可以得到 60fps 的移动。

-  drawingNode && drawingNode.batchDraw();
+  drawingNode && drawingNode.draw();

根据他们的文档,batchDraw 会绘制the next animationFrame。但react它本身也用于RAF触发下一次道具更新,所以batchDraw这里发生2 frames在 i 之后setHeroPos()

解决方案:

我要向他们的项目提交一个拉取请求。

标签: reactjsperformancehtml5-canvasframe-ratereact-konva

解决方案


开发工具可以在设备上增加很多额外的负载。当您记录性能日志时更是如此。

React 是我用于实时应用程序的最后一件事,因为它将幕后 JS 分配到甚至最简单的任务中。

通过测量帧之间的时间来计算性能并不能准确地指示性能。

表现

要测量函数的性能,请使用performanceAPI。最简单的方法是通过performance.now使用它来获取功能完成所需的时间。

例如获取游戏中主循环函数的时间

function mainLoop(frameTime) {
    const now = performance.now(); // MUST BE FIRST LINE OF CODE TO TEST!!!!

    requestAnimationFrame(mainLoop);
    const executeTime = performance.now() - now; // MUST BE LAST LINE OF CODE TO TEST!!!
}

这将为您提供以毫秒为单位的执行时间。因为 JS 是阻塞的,所以只测量两行内的代码。

  • 注意没有测量额外开销,例如 GC、合成、同步加载等......

  • 注意毫秒(1/1,000,000th)

  • 注意此值的精度performance.now被故意降低以保护用户,并且根据浏览器的不同在 100 毫秒 - 200 毫秒之间(可以在标志和系统配置后面访问 1 毫秒))

有意义的表现

JS 执行是不确定的,这使得单个计时测量完全不可靠。(为什么它比使用更好的performance.now原因peformance.mark

为了克服 JS 执行的不确定性和计时器的不准确性,请使用运行平均值来计时您的代码。下面的示例显示了如何执行此操作。

与其显示时间,不如使用与应用程序需求相关的指标。例如,执行代码花费了多少帧。(见示例)

例子

此示例使用requestAnimationFrame.

滑块允许您选择函数应该花费渲染的大约时间。

顶部的信息文本将计时结果显示为运行平均值。

您会注意到理想化帧负载 (IFL) 在帧速率下降之前远低于 100%。

实验

  1. 开发工具和性能监控如何影响性能。

当帧速率低于 60 时,将滑块移动到正下方。

打开开发工具,看看它是否以及如何影响明显的性能。记下任何变化。是否有影响,如果有,影响多少?

记录性能日志并查看 FPS 和/或 IFL 是否受记录影响

  1. 在影响帧速率之前,您的设备可以分配给渲染的最长时间是多少。

将滑块缓慢向右移动。

当帧速率低于 60 时,将幻灯片向后移动一步,直到它再次读取 60FPS。

IFL将给出完美帧(第 60 秒)执行代码的百分比。Time绝对执行时间,以毫秒为单位。

Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randItem = arr => arr[Math.random() * arr.length | 0];
CPULoad.addEventListener("input",() => loadTimeMS = Number(CPULoad.value));
var loadTimeMS = Number(CPULoad.value);
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);

function mainLoop(frameTime) {

    /* Timed section starts on next line */
    const now = performance.now();
    
    CPU_Load(loadTimeMS);
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    requestAnimationFrame(mainLoop);
    
    const exeTime = performance.now() - now;
    /* Timed section ends at above line*/

    measure(info, frameTime, exeTime);
    
}

const measure = (() => {
    const MEAN = (t, f) => t += f;
    const fTimes = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], bTimes = [...fTimes];
    var pos = 0, prevTime, busyFraction;
    return (el, time, busy) => {
        if (prevTime) {
            bTimes[pos % bTimes.length] = busy;		
            fTimes[(pos ++) % fTimes.length] = time - prevTime;		
            const meanBusy = bTimes.reduce(MEAN, 0) / bTimes.length;
            const meanFPS = fTimes.reduce(MEAN, 0) / fTimes.length;
            el.textContent = "Load: " + loadTimeMS.toFixed(1) + "ms " +
                " FPS: " + Math.round(1000 / meanFPS) + 
                " IFL: " + (meanBusy / (1000 / 60) * 100).toFixed(1) + "%" +
                " Time: " + meanBusy.toFixed(3) + "ms";
            busyFraction = meanBusy / (1000/60);
        }
        prevTime = time;
    };
})();

const colors = "#F00,#FF0,#0F0,#0FF,#00F,#F0F,#000,#FFF".split(",");
// This function shares the load between CPU and GPU reducing CPU
// heating and preventing clock speed throttling on slower systems.
function CPU_Load(ms) { // ms = microsecond and is a min value only
   const now = performance.now();
   ctx.globalAlpha = 0.1;
   do {
      ctx.fillStyle = Math.randItem(colors);
      ctx.fillRect(Math.rand(-50,250), Math.rand(-50, 100), Math.rand(1, 200), Math.rand(1,100))
   } while(performance.now()-now <= ms);
   ctx.globalAlpha = 1;
}
body {
  font-family: arial;
}
#info {
    position: absolute;
    top: 10px;
    left: 10px;
    background: white;
    font-size:small;
    width:345px;
    padding-left: 3px;
}
#canvas {
    background: #8AF;
    border: 1px solid black;
}
#CPULoad {
    font-family: arial;
    position: absolute;
    top: 130px;
    left: 10px;
    color: black;
    width: 340px !important;
}
<code id="info"></code>
<input id="CPULoad" min="0" max="36" step="0.5" value="2"  type="range" list="marks"/>
<canvas id="canvas" width="350"></canvas>
<datalist id="marks">
  <option value="0"></option>
  <option value="4"></option>
  <option value="8"></option>
  <option value="12"></option>
  <option value="16"></option>
  <option value="20"></option>
  <option value="24"></option>
  <option value="28"></option>
  <option value="32"></option>
  <option value="36"></option>
</datalist>

注意时间的显示会影响结果。此代码在沙盒代码段中运行的事实将影响结果。为了获得最准确的结果,请在独立页面上运行代码。将结果记录到 JS 数据结构中,并在测试运行后显示结果。

  • 负载:请求的 CPU/GPU 执行负载在 1/1000 秒内。

  • FPS:运行平均每秒帧数。

  • IFL:理想化帧负载,第 60 秒执行代码的百分比。

  • 时间:平均测量的执行时间,以 1/1000 秒为单位。


推荐阅读