为什么我在 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?
我正在尝试用react开发经典游戏教程how-to-make-a-simple-html5-canvas-game。
一切顺利,直到我发现我的动作有点问题,online test link and code。
而原来的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"
/>
定位问题
我添加了一个对比 canvas,它会在每次更新 heroPos
时呈现。它让我在 chrome 开发工具中达到 60FPS。现在问题肯定是由我正在使用的 canvas 库引起的: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();
根据他们的 doc,batchDraw 会在 the next animationFrame
中绘制。但是 react
本身也使用 RAF
来触发下一个道具更新,所以这里的 batchDraw
发生在我 setHeroPos()
.
之后 2 frames
解决方案:
我要向他们的项目提交 pull-request。
开发工具会给设备增加很多额外的负载。当您记录性能日志时更是如此。
React 是我最不想用于实时应用程序的东西,因为它为甚至最简单的任务添加了幕后 JS 的分配。
通过测量帧之间的时间来计算性能并不能准确指示性能。
性能
要衡量函数的性能,请使用 performance
API. The simplest way is via 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,Compositing,同步加载等等...
注意 毫秒(1/1,000,000)
注意 这个值的精度 performance.now
has deliberately been reduced 是为了保护用户,在 100 毫秒到 200 毫秒之间的任何地方,具体取决于浏览器(1 毫秒可以是在标志和系统配置后面访问))
有意义的表现
JS 执行是不确定的,这使得单个时间测量完全不可靠。 (为什么最好使用 performance.now
than peformance.mark
)
为了克服 JS 执行的不确定性和计时器的不准确性,请使用 运行ning 方法为您的代码计时。下面的示例显示了如何执行此操作。
使用与应用程序需求相关的指标而不是显示时间。例如,一帧有多少时间用于执行代码。 (见示例)
例子
此示例使用 requestAnimationFrame
.
对某些 canvas 内容进行动画处理
滑块可让您 select 函数应该花在渲染上的大约时间量。
顶部的信息文本将计时结果显示为 运行宁平均值。
您会注意到在帧速率下降之前,理想化帧负载 (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" 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 秒执行代码的百分比。
Time:平均测量执行时间,以 1/1000 秒为单位。
我正在尝试用react开发经典游戏教程how-to-make-a-simple-html5-canvas-game。
一切顺利,直到我发现我的动作有点问题,online test link and code。
而原来的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"
/>
定位问题
我添加了一个对比 canvas,它会在每次更新 heroPos
时呈现。它让我在 chrome 开发工具中达到 60FPS。现在问题肯定是由我正在使用的 canvas 库引起的: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();
根据他们的 doc,batchDraw 会在 the next animationFrame
中绘制。但是 react
本身也使用 RAF
来触发下一个道具更新,所以这里的 batchDraw
发生在我 setHeroPos()
.
2 frames
解决方案:
我要向他们的项目提交 pull-request。
开发工具会给设备增加很多额外的负载。当您记录性能日志时更是如此。
React 是我最不想用于实时应用程序的东西,因为它为甚至最简单的任务添加了幕后 JS 的分配。
通过测量帧之间的时间来计算性能并不能准确指示性能。
性能
要衡量函数的性能,请使用 performance
API. The simplest way is via 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,Compositing,同步加载等等...
注意 毫秒(1/1,000,000)
注意 这个值的精度
performance.now
has deliberately been reduced 是为了保护用户,在 100 毫秒到 200 毫秒之间的任何地方,具体取决于浏览器(1 毫秒可以是在标志和系统配置后面访问))
有意义的表现
JS 执行是不确定的,这使得单个时间测量完全不可靠。 (为什么最好使用 performance.now
than peformance.mark
)
为了克服 JS 执行的不确定性和计时器的不准确性,请使用 运行ning 方法为您的代码计时。下面的示例显示了如何执行此操作。
使用与应用程序需求相关的指标而不是显示时间。例如,一帧有多少时间用于执行代码。 (见示例)
例子
此示例使用 requestAnimationFrame
.
滑块可让您 select 函数应该花在渲染上的大约时间量。
顶部的信息文本将计时结果显示为 运行宁平均值。
您会注意到在帧速率下降之前,理想化帧负载 (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" 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 秒执行代码的百分比。
Time:平均测量执行时间,以 1/1000 秒为单位。