While investigating a site-speed anomaly, I discovered a critical bug that was silently breaking performance monitoring for potentially many WordPress sites.
update: this issue has been addressed in WP Rocket release 3.20.4 by moving to dns-prefetch. New LinkedIn post covering this.
To put this in perspective: enabling WP Rocket caused data points to plummet from 6.3M to just 214K for the same timespan.
Which is unexpected as the Time to First Byte (TTFB) metric should always come before the First Contentful Paint (FCP) event. After all, the browser can only render things when it at least received the first few bytes.
Meanwhile, TTFB is a very challenging metric for many WordPress sites. So, having reliable monitoring data is critical for WP site owners.
Down the RocketLazyLoadScripts rabbit hole
I investigated the issue. When checking the source code of the domain, I saw that the RUMvision monitoring snippet (which relies on the web-vitals library) was loaded using an inline JavaScript with type=rocketlazyloadscript attribute.
In other words, this website was using WP Rocket to delay JavaScript execution.
Web Vitals library
I first thought it was a bug coming from Google Chrome's web-vitals library. Maybe the web-vitals was conflicting with WP Rocket's lazyload feature.
To test this, I used the overrides feature in Chrome DevTools and removed the type=rocketlazyloadscript attribute from the inline script that was embedding the RUMvision snippet.
TTFB suddenly started to be reported. I then specifically checked the source code to see how web-vitals is reporting the TTFB metric. Simplified conclusion:
- they use a
loadevent listener:addEventListener('load', ..) - If the
loadevent has already happened (checked viadocument.readyState), they dispatch TTFB immediately.
Scenario 2 is likely to happen when a site is embedding the web-vitals library late in time. That's the advantage of modern performance APIs after all: the web-vitals library is basically a third party JavaScript file, but doesn't need to be given high priority to still allow it to collect valuable data. As it should be when talking about third parties and their sitespeed impact.
Simple building-block test
To test if web-vitals was the one conflicting here, I went back to DevTools overrides. This time around, I copied the whenReady logic and tested it separately by pasting it into the the type=rocketlazyloadscript script.
Result: load event handler never got executed.
WP Rocket LazyLoadScripts
To be able to understand what could be happening, it's convenient to know how WP Rocket LazyLoadScript works.
- WP Rocket will rewrite your scripts into
type=rocketlazyloadscript. - Those don't count as JS until a user interacts with the page.
- At that moment WP Rocket reinjects them so they finally execute.
- But if those scripts add a
loadevent listener after the realwindow.loadhas already fired, their callbacks will never run in certain cases.
document.readyState
WP Rocket overrides the document.readyState to loading and then introduces custom events. Even the advanced check used in the web-vitals library fail in this scenario:
- when executed late in time,
document.readyState !== 'complete'results in true because it was set toloading; - web-vitals library thinks it should set an event listener, but the
loadevent already happened in reality and will never end up being executed.
Interacting before the load event
It's important to note that when users started to interact before the actual load event happened, everything works as expected. That's because WP Rocket will then already reinject your JS before the actual load event as well.
But, the chances of this happening are very slim as most JavaScript ends up being lazyloaded when using WP Rocket, so the load event has a higher chance to happen way sooner in the page life cycle.
Based on the numbers shared earlier, I calculated that users interacted (hover, scroll or click) with the page before the load event in only 3.8% of sessions (roughly 1 in 24.5 page hits).
Result: TTFB very often doesn't get dispatched, leading to underreported TTFB events.
Other metrics will end up being reported as TTFB is the only metric that is dispatched by looking at the readyState value or using an load event listener, instead of being buffered.
Scope matters
When I tried to test another load event listener, things seemed to work, which was puzzling. So I ended up writing a WP Rocket LazyLoadScripts demo-page to test this.
I then discovered that the scope of setting the load event matters. This will fail:
addEventListener('load', ..)
But when load event listeners are attached to the window scope, it will end up being executed:
window.addEventListener('load', ..)
I ended up adding an additional remark to an issue I already created at WP Media's github page.
The culprit
When looking at their code, the line shown below is the culprit, showing it will only handle load event listeners when specifically attached to the window:
e === window && "load" === t
As a result, WP Rocket won't tag it as a window listener and won't wire it to their replayed events.
The fix
The fix should ideally come from the WP Media team, responsible for WP Rocket plugin. As a matter of fact, it might already become part of the next Delay JavaScript Execution script update:
Should be there on the next Delay JavaScript Execution script update
Adame Dahmani on LinkedIn
For now, if you are either a third party doing this or a site owner knowing of scripts doing this, be aware that your code might not execute as expected on sites using WP Rocket's RocketLazyLoadScripts. Especially when collecting events, it might unknowingly fail or underreport until both a fix will be there and sites using the Delay JavaScript Execution feature have updated their plugin as well.



