While doing some field research for a talk he gave at #PerfMatters Conference, my colleague @simonhearne made an interesting discovery.

Was there really “beef” between two Javascript analytics agents? How did it resolve? Were there tridents involved? Did everyone comply with the no touching of the hair or face? Nerd Beef (source: https://i.pinimg.com/originals/e9/0a/4f/e90a4f180be7d3156aab7de1026b4e48.png)

Let’s unpack @simonhearne’s tweet a bit.

[Full disclosure: I worked at New Relic (on the Infrastructure monitoring product) and enjoyed my time there immensely.]

The problem

As well documented in the actual bug report, the web performance experts I work with noticed that our javascript agent was reporting fewer resources than expected on a given page that we co-habitated with the New Relic Browser agent. We tracked it down to a resourcetimingbufferfull handler calling clearResourceTimings() directly. Boomerang prefers to collect the PerformanceResourceTiming entries at beacon time, by which point - had the typical 150 limit been reached - we would be missing out on the first most critical resources after navigation. So that wasn’t ideal.

Why we are the way we are

Boomerang uses two buffer APIs, but both require user opt-in. We provide UI on top of:

Our identity has always been to be the passive listener, affecting as little of the page as possible so as to minimize Observer Effect. But - also to be good citizens. We don’t want to cause any side affects to the page that could ever be noticed by either the the first party itself or any other third party. As a third party ourselves, we’ve been broken in so many ways by other side affect causing third parties that it’s staggering. The last thing in the world we want to do is to step on someone else’s toes.

Which is why, anything that will cause side affects - we make opt-in.

Barring opt-in, we consider a third-party clearing the resource timing buffer an example of “not playing nicely together”.

The other side

I reached out to a few people at New Relic and finally tracked down Ty Herbert, the product manager for Browser. Ty very quickly said that they weren’t clearing the buffer to keep the entries from other scripts on the page - they just do it to make sure that they don’t lose any entries when the limit is reached. More from Ty:

We understand that clearing the resource timing may not be ideal for auxiliary products which utilize the resource timing data. Weโ€™re moving to implement greater user control over when resource timing is cleared, and will continue to improve the product guided by customer feedback.

It sounds to me like they might sometime in the future explore making their clearResourceTimings call a user setting - which is great, but a change won’t come over night. In the meantime…

The defense

To protect our PerformanceResourceTiming entry collection from being marginalized, we added a feature (off by default) that will trap clearResourceTimings() so we don’t miss a thing. This would be a safe approach for anyone that reads the performance timeline to apply (it will daisy-chain). Something like:

1
2
3
4
5
6
7
8
9
if ('performance' in window && typeof performance.clearResourceTimings === 'function') {
  performance.clearResourceTimings = (function(_clearResourceTimings){
    return function() {
      // collect entries with `performance.getEntriesByType('resource')`
      return _clearResourceTimings.apply(performance, arguments)
    }
  })(performance.clearResourceTimings)
}

The real answer

… is of course PerformanceObserver. With the to-be-implemented buffered flag, this decently supported API will allow the web platform to someday have the opportunity to deprecate and remove the racy getEntries*() methods.

In the meantime, please don’t clear the resource timing buffer unless site owner has explicitly opted-in. And, until all of the browsers that you support implement PerformanceObserver, it’s probably best to be defensive during collection.

Lastly, excepting long-running SPAs), maybe we should consider this as a more civilized response to a resourcetimingbufferfull event:

1
2
3
4
5
if (typeof performance.setResourceTimingBufferSize === 'function') {
  performance.setResourceTimingBufferSize(
    2 * performance.getEntries().length)
}