I'm making a multiplayer web game similar to Snake. The goal is to broadcast a gamestate update from the server to all clients every 200ms, in response to which the clients immediately draw the gamestate to achieve roughly a constant 5fps update rate. I implemented this using Websockets and it worked as expected on multiple laptops connected to my LAN, but whenever I join on my mobile browser (Chrome 101.0.4951.41 on Android 11) the framerate is inconsistent and the game appears to lag (e.g. the average period will still be 200ms, but two frames will be 100ms apart followed by 300ms until the next frame).
Initially, my backend was written in Go using the gobwas/ws package with wsutil. I verified that the server is sending messages 199-201ms apart, and tried writing a new backend to do the bare minimum (broadcast every 200ms) using node.js. I eliminated all client-side logic except logging the elapsed time and calculating the min, mean, max, and standard deviation. The problem persisted, indicated by high range and standard deviation on the mobile device. I switched to using socket.io in case it was a Websockets problem. The problem persisted.
Code
The complete code is below. I am running the backend using node.js v18.1.0 on Arch Linux 5.17-1-arch1-1.
Server Node.js Code (file: send-socketio.js)
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
setInterval(() => { io.emit('update', {}) }, 200);
server.listen(3000, () => {
console.log('listening on *:3000');
});
Client HTML and Javascript (file: index.html)
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="msgs"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
var dts = [];
const socket = io();
var last_msg_time = null;
function getStandardDeviation(arr) {
const n = arr.length;
if (n == 0) {
return 0;
}
const mean = arr.reduce((a, b) => a + b) / n;
return Math.sqrt(arr.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
}
function getMean(arr) {
const n = arr.length
if (n == 0) {
return 0;
}
return arr.reduce((a, b) => a + b) / n
}
socket.on('update', function(msg) {
var now = new Date();
if (last_msg_time != null) {
var dt = now - last_msg_time;
dts.push(dt);
let ele = document.getElementById('msgs');
ele.innerHTML = `<ul><li>min:${Math.min(...dts)}</li>
<li>avg:${getMean(dts)}</li>
<li>max:${Math.max(...dts)}</li>
<li>std:${getStandardDeviation(dts)}</li>
</ul>`;
}
last_msg_time = now;
});
</script>
</body>
</html>
Results
With all three devices running simultaneously for a similar duration of time, the displayed results were as follows (with 200ms update frequency, all units in ms):
Client 1: localhost
min:152
avg:200.23478260869564
max:202
std:2.3427239247345715
Client 2: other laptop on LAN
min:167
avg:200
max:212
std:4.78
Client 3: mobile phone on LAN, Chrome browser (similar results in Brave)
min:55
avg:200.7
max:374
std:52.14
Note especially how much larger the time delta range and stddev is on the mobile browser. All devices are connected to the same wireless LAN, in the same room and very close together.
I don't think this is a bottleneck in processing the messages on either the client or the server side, since each message is just 'update' with an empty body, and messages are only sent every 200ms. When I decrease the server broadcast period to 20ms, the mean dt also decreases to ~20ms on all devices, but the standard deviation remains comparably high on the mobile browser (~10ms instead of ~1ms).
If anyone has an idea what is going on here or a possible solution in mind, I would be grateful to hear it. Right now the only solution I have in mind is to flood each client with messages (e.g. broadcast every 20ms instead of 200ms to reduce the standard deviation to 10ms instead of 50ms) but this is very inefficient and hacky.
Thanks!