From 6aab63e99117531bc57dccd9afd66e7a64a04493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Wed, 10 Dec 2025 12:44:57 +0100 Subject: [PATCH] Fix performance issues by limiting fps --- src/main/resources/static/js/broadcast.js | 49 +++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/resources/static/js/broadcast.js b/src/main/resources/static/js/broadcast.js index 49edd55..7cd4c7f 100644 --- a/src/main/resources/static/js/broadcast.js +++ b/src/main/resources/static/js/broadcast.js @@ -8,7 +8,14 @@ const mediaCache = new Map(); const renderStates = new Map(); const animatedCache = new Map(); const audioControllers = new Map(); -let animationFrameId = null; +const TARGET_FPS = 60; +const MIN_FRAME_TIME = 1000 / TARGET_FPS; +let lastRenderTime = 0; +let frameScheduled = false; +let pendingDraw = false; +let sortedAssetsCache = []; +let assetsDirty = true; +let renderIntervalId = null; const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('Audio'); function connect() { @@ -28,6 +35,7 @@ function renderAssets(list) { asset.zIndex = Math.max(1, asset.zIndex ?? 1); assets.set(asset.id, asset); }); + assetsDirty = true; draw(); } @@ -71,16 +79,45 @@ function handleEvent(event) { clearMedia(event.payload.id); renderStates.delete(event.payload.id); } + assetsDirty = true; draw(); } function draw() { + if (frameScheduled) { + pendingDraw = true; + return; + } + frameScheduled = true; + requestAnimationFrame((timestamp) => { + const elapsed = timestamp - lastRenderTime; + const delay = MIN_FRAME_TIME - elapsed; + const shouldRender = elapsed >= MIN_FRAME_TIME; + + if (shouldRender) { + lastRenderTime = timestamp; + renderFrame(); + } + + frameScheduled = false; + if (pendingDraw || !shouldRender) { + pendingDraw = false; + setTimeout(draw, Math.max(0, delay)); + } + }); +} + +function renderFrame() { ctx.clearRect(0, 0, canvas.width, canvas.height); getZOrderedAssets().forEach(drawAsset); } function getZOrderedAssets() { - return Array.from(assets.values()).sort(zComparator); + if (assetsDirty) { + sortedAssetsCache = Array.from(assets.values()).sort(zComparator); + assetsDirty = false; + } + return sortedAssetsCache; } function zComparator(a, b) { @@ -480,14 +517,12 @@ function applyMediaSettings(element, asset) { } function startRenderLoop() { - if (animationFrameId) { + if (renderIntervalId) { return; } - const tick = () => { + renderIntervalId = setInterval(() => { draw(); - animationFrameId = requestAnimationFrame(tick); - }; - animationFrameId = requestAnimationFrame(tick); + }, MIN_FRAME_TIME); } window.addEventListener('resize', () => {