如何防止JavaScript锁定浏览器?

3
我有一个WebSocket,以大约50hz的速率接收数据。每次将更新推送到浏览器时,它会将JSON数据转换为一些漂亮的图表。
$(document).ready(function() {
    console.log('Connecting web socket for status publishing')
    allRawDataPublisher = new ReconnectingWebSocket("ws://" + location.host + '/raw/allrawdata');

    var rawUnprocessedData = [];

    for (i = 0; i < 256; i++)
    {
        rawUnprocessedData.push({x:i, y:0});
    }

    var unprocessedRawChart = new CanvasJS.Chart("rawUnprocessedData",{
        title :{ text: "Raw Unprocessed Data"},
        axisX: { title: "Bin"},
        axisY: { title: "SNR"},
        data: [{ type: "line", dataPoints : rawUnprocessedData},{ type: "line", dataPoints : noiseFloorData}]
    });

    var updateChart = function (dps, newData, chart) {
        for (i = 0; i < 256; i++)
        {
          dps[i].y = newData[i];
        }
        chart.render();
    };

    allRawDataPublisher.onmessage = function (message) {
        jsonPayload = JSON.parse(message.data);
        var dataElements = jsonPayload["Raw Data Packet"]
        updateChart(rawUnprocessedData, dataElements["RAW_DATA"].Val, unprocessedRawChart)
    };

    unprocessedRawChart.render();
});

当我的笔记本电脑插入电源时,这很有效,但是如果我拔掉电源,我的笔记本电脑的处理能力会下降(较低规格的平板电脑、手机等也存在同样的问题)。当可用的处理能力较少时,浏览器(Chrome)完全锁定。
我猜想 javascript 接收更新的速度比浏览器渲染它们的速度快,因此导致选项卡被锁定。
如果浏览器无法按请求的速率更新,我希望它在准备好渲染新更新之前放弃新数据。是否有一种标准方法来检查浏览器是否有足够的时间来渲染更新,并在这种情况下丢弃新帧?
*[编辑]
我使用 Chrome 的分析器进行了一些挖掘,确认(如预期)重新绘制图表占据了大部分处理能力。

@TahaPaksu setTimeout不会启动任何单独的进程-它只是使回调函数的执行异步化。 - nicholaswmin
是的,但它会以某种方式防止页面被阻止吗? - Taha Paksu
我不确定,但也不能百分之百肯定 - 我猜事件队列仍然会被淹没 - 在某个时刻,那些 setTimeout 将会到期并调用它们的回调函数,从而导致相同的问题。 - nicholaswmin
你应该研究一下Web WorkersDebouncing - David Hellsing
听起来你只是试图在主线程中以50Hz进行过多的处理。你可以将网络、解析和计算时间转移到webWorker中,但不能转移渲染或DOM工作。因此,这取决于进程的不同部分需要多少时间,是否使用webWorker会有所帮助。大多数情况下,我猜你需要削减要处理的数据流,这样就不需要处理那么多数据了。 - jfriend00
显示剩余2条评论
1个回答

2
您可以使用window.requestAnimationFrame在帧之间执行工作。
传递给此函数的回调将最多每秒调用60次 - 或与您的显示器的刷新率匹配的任何数字。
它还保证在下一次重绘之前被调用,并且在上一次重绘完成后被调用。
来自MDN window.requestAnimationFrame()

window.requestAnimationFrame()方法告诉浏览器您希望执行动画,并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法接受回调作为参数,在重绘之前调用。

以下是使用示例:

function renderChart () {
   // pull data from your array and do rendering here
   console.log('rendering...')
   requestAnimationFrame(renderChart)
}

requestAnimationFrame(renderChart)


然而,最好的做法是批量渲染您的图形,而不是为每个数据或每一帧都进行渲染。

这里有一个使用Chart.js代码的Fiddle,它:

  • 每100ms(10 Hz)将数据推送到一个Array
  • 以4个数据为一组进行渲染,每1000ms(1 Hz)进行一次批量渲染

const values = []
const ctx = document.getElementById('chartContainer').getContext('2d');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['start'],
    datasets: [{
      label: 'mm of rain',
      data: [1],
      borderWidth: 1
    }]
  }
});

// Push 1 item every 100ms (10 Hz), simulates 
// your data coming through at a faster
// rate than you can render
setInterval(() => {
  values.push(Math.random())
}, 100)

// Pull last 4 items every 1 second (1 Hz)
setInterval(() => {
  // splice last 4 items, add them to the chart's data
  values.splice(values.length - 4, 4)
    .forEach((value, index) => {
      chart.data.labels.push(index)
      chart.data.datasets[0].data.push(value)
    })

  // finally, command the chart to update once!
  chart.update()
}, 1000)

请注意,上述概念需要适当处理异常情况,否则values Array将开始积累大量数据,导致进程内存不足。
您还需要小心地呈现批次。如果您的值渲染速率比您填充values Array的速率慢,最终您将遇到内存问题。
最后但并非最不重要的一点是:我真的不认为您需要以每秒2次的速度更新数据,因为我怀疑人脑无法在如此快的速度下进行有用的解释。

这似乎是一种更合理的方法;将绘图更新与数据流分离,并将其降至浏览器可以处理的速率。 - Jon Cage
是的 - 然而这并不意味着你一定会得到一个流畅的用户体验。浏览器仍然会忙于尝试进行渲染,这会留下一小部分CPU周期(为了简洁而过度简化)用于其他事情。如果您的数据差异很小,您可能还想考虑我在答案中提到的“批处理”方法。 - nicholaswmin
我猜类似这个 - 这是非常简陋的代码,但我的做法会类似这样。你可能想尝试一下渲染3或4个批次而不仅仅是2个。 - nicholaswmin
我之前应该问过,你是否对代码进行了分析以确保渲染是导致锁定的原因? - nicholaswmin
我最终使用了setTimeout而不是setInterval来更新图表,并将图表更新分离到该超时中,从而解决了我的问题。现在浏览器不会锁定,它只会尽可能多地更新并丢弃其余的更新。这里有一个演示一些内容的fiddle: https://jsfiddle.net/g05xcx0g/9/ ,虽然它也为演示目的生成数据。 - Jon Cage
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接