且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

websocket 的可怕性能问题 - 每条消息都会触发 Angular 变化检测

更新时间:2023-02-06 16:01:01

Sods 法则,我一发布问题就想出答案.通常我会删除它,但我找不到关于该主题的太多内容,因此它可能对某人有用.

这是区域的问题.这是一篇有用的博文.一般来说,Angular 2 使用区域来驱动它的变化检测:

Angular 2 使用 zone.js 的原因是要知道我们的处理程序何时完成,然后 NgZone 服务(NgZone 是一个包装区域服务的类)调用 ApplicationRef.tick() 方法.tick() 方法从上到下扫描树组件,并在每个组件中计算模板中存在的表达式.如果表达式的结果不等于前一个结果,(从前一个刻度开始)Angular 将更新连接到这个表达式的 DOM 属性.

简单地单独获得消息性能的一个简单解决方案是在区域外运行,从而使用方便的方法runOutsideAngular:

this.zone.runOutsideAngular(() => {让我 = 0;this.socket.onmessage = (e: MessageEvent) =>{我++;如果(我 % 100 === 0){console.log('收到' + i);}};});

I've got a websocket and I'm currently streaming messages at about 45 per second. At this level it completely kills chrome. All functionality in the browser window is locked up.

I've cut back my implementation to try find the root cause of this issue, suspecting it could have been some problem with how I'm processing these messages - or updating/change detection in Angular.

But now I'm left with the simplest websocket implementation I can get:

this.socket = new WebSocket('ws://localhost:5000/ws');

let i = 0;
this.socket.onmessage = (e: MessageEvent) => {
  i++;
  if (i % 100 === 0) {
    console.log('recieved' + i);
  }
};

This is in an Angular Injectable, but it's not interacting with anything.

It writes to the console every 100 messages. This still kills the browser with 100% CPU usage, it doesn't even output to the console most of the time until I stop the message stream, then everything catches up and it squirts a few lines of "recieved x00" messages.

The messages themselves are JSON, and look like this:

{
  "Topic":"2a736d15-a2fe-43b2-8e8b-ee888f15a53a","Type":1,
  "Message": {
     "Value":2,
     "Timestamp":"2017-06-05T14:46:21.615062+01:00"
   }
}

They're typically about 126 characters.

I'd assumed websockets can handle tens of thousands of messages per second, but I can't find any sensible metrics on google, does this sound like unreasonable performance?

I've also checked in the CPU profiler. When everthing's locked out, it's just a big wall of calls, so I turned it down to a single message a second:

Drilled down into one of these spikes (happens for every single message!):

I threw a break point in one of these locations, and it appears that Angular is doing something whenever the websocket gets triggered, and it's something to do with Zones (something I have zero experience in!):

The first thing in the stack is a ZoneTask.invoke on the Websocket itself, but then NgZone gets triggered, and ultimately a very expensive change/update chain is called:

How could all this be happening? The 7 lines of code subscribing to the websocket are completely independent from Angular's change detection right? They're not changing any values that are bound to any components. Their only link is they are sitting in a method inside an @Injectable()

Sods law, as soon as I post a question I work out the answer. Usually I'd delete it but I couldn't find much on the topic so it might be useful for someone.

It was an issue with Zones. Here's a useful blog post. In general, Angular 2 uses zones to drive it's change detection:

The reason that Angular 2 uses the zone.js is to know when our handler finishes and then the NgZone service (NgZone is a class that wrap the zone service) calls to ApplicationRef.tick() method. The tick() method scans the tree components from top to bottom and calculates in each component the expressions that exist in the template. If the result of the expression is not equal to the previous result, (from the previous tick) Angular will update the DOM property that connects to this expression.

An easy solution to simply get the message performance in isolation is to run outside of the zone, and thus angular's change detection with the handy method runOutsideAngular:

this.zone.runOutsideAngular(() => {
  let i = 0;
  this.socket.onmessage = (e: MessageEvent) => {
    i++;
    if (i % 100 === 0) {
      console.log('recieved' + i);
    }
  };
});