为啥我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说是 60?

Posted

技术标签:

【中文标题】为啥我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说是 60?【英文标题】:Why I get 30 fps in chrome dev tool performance panel, but JS and React both say it's 60?为什么我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说是 60? 【发布时间】:2020-06-13 11:36:44 【问题描述】:

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

一切都很顺利,直到我发现我的动作有点小故障,online test link 和 code。

虽然原来用JS写的game要流畅得多:

所以我稍微研究了一下,发现实际的 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]);

发现问题

我找到了问题,是使用了batchDrawreact-konva引起的:

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

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

根据他们的doc,batchDraw 将绘制the next animationFrame。但是react本身也使用RAF来触发下一次props更新,所以这里的batchDraw发生在我2 frames之后setHeroPos()

解决方案:

我要向他们的项目提交pull-request。

【问题讨论】:

您应该使用您找到的内容发布自我答案,而不是将其添加为编辑。它可能对未来的读者也很有用,它比当前接受的答案更好地解释了为什么 30FPS 而不是 60FPS。 【参考方案1】:

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

React 是我用于实时应用程序的最后一件事,因为它为最简单的任务添加了幕后 JS。

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

性能

要测量函数的性能,请使用performance API。最简单的方法是通过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,000)

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

有意义的表现

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

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

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

示例

此示例使用requestAnimationFrame 为一些画布内容设置动画。

滑块可让您选择函数应花费渲染的大致时间。

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

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

实验

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

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

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

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

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

将滑块缓慢向右移动。

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

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

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" ></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>

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

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

FPS:平均每秒帧数。

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

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

【讨论】:

React 可以很好地进行反应。您只需要考虑很多性能,例如PureComponents、React.memo 等 很好的解释。我尝试了performance.now(),但它最终向我显示了更准确的时间(从 16 毫秒到 16.213123 毫秒)。但这并不能解决我的问题... @Stupidism 16ms 太长了。示例代码将为您估计一帧需要多长时间才能保持恒定的帧速率。对于普通设备,它约为 60 - 70%,低端设备下降,高端设备更高。原因是浏览器需要时间来完成它的工作(合成、可能的回流、事件、GC 等等)。我确实暗示了一个解决方案,React 不适合实时应用程序 @Blindman67 感谢您耐心的解释。感谢您不仅为我调试,还教导和指导我找到答案。 "注意:毫秒 (1/1,000,000th) 注意:此值性能的精度。现在 [...] 介于 100 毫秒 - 200 毫秒之间”。哼什么? FF 具有最大的舍入因子,只有一个毫秒。 Chrome 有一些抖动,但仍处于微秒精度。 200 毫秒的精度将使秒表更有用:-)

以上是关于为啥我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说是 60?的主要内容,如果未能解决你的问题,请参考以下文章

chrome开发者工具网络面板中status为啥意思

为啥我的 Ajax 调用的“等待”长度如此之长? (Chrome 网络面板)

chrome开发者工具网络面板中status为啥意思

chrome开发者工具网络面板中status为啥意思

chrome开发者工具网络面板中status为啥意思

Chrome开发者工具:利用网络面板做性能分析