Page MenuHomePhabricator

ext.popups.main loads after most of all JS in desktop
Open, Needs TriagePublic


After the addition of requestIdleCallback and the separation between ext.popups and ext.popups.main, our code is delayed and is loaded after basically everything else (collapsible tabs, universal language selector on the sidebar, the video player piece, etc).

One could argue that Popups are likely to be interacted with in the content earlier than many of those other features and that they shouldn't block the execution of the feature.

Tested in while logged in, the small piece in the timeline is when popups finally runs after seconds of other scripts doing things:

Tested in logged out. Same thing, the small piece in the timeline at the end is when it runs:

It seems like rIC is hurting the feature if it is the only piece of JS that is delayed and scheduled to load like that.


  • Should popups the same scheduling mechanism that other scripts/features use instead of rIC (setTimeout or requestAnimationFrame could schedule it earlier)? (if they use any)
  • Should all other scripts on the page are also scheduled with requestIdleCallback to be loaded more fairly?
  • Should we examine the different scripts loaded and:
    • Either find a way to specify priorities and properly schedule script loading based on the priority
    • Standardize non-critical features with rIC loading like we were recommended to do with popups

Event Timeline

@Krinkle @Gilles our biggest concern here is that there is other JavaScript on the critical path which is arguably less important for a person on a slower connection than page previews. One of the main motivators of this feature is that we can save page views for users on slower connections, so it's unfortunate that other features which do not help those users impact loading of page previews (for example video playing is quite clearly more useful for users on a fast connection).

Should features such as collapsible tabs, universal language selector on the sidebar, the video player piece, etc also use the same deferred loading/ requestIdleCallback behavior?

Background on rIC

Despite the generic word "idle" in requestIdleCallback, it does not refer to a distant time where nothing else can happen. Quite the contrary.

requestIdleCallback is typically in the same tick of the event loop as requestAnimationFrame. Both these and setTimeout all happen typically within a few milliseconds. rIC can actually fire much earlier, in <1ms whereas timers tend to have a minimum of 4ms, and animations around 16ms (assuming 60fps)

I recommend as excellent resource on figuring out all the internals of the event loop. TL;DW: It's pretty hard to predict what a browser will do without understanding all the internals. But the good news is, that, the browser generally makes the best decisions for UX and performance, if code follows a set of principles. For example, one should use rAF for making DOM changes, and use rIC for any pure-JS work that shouldn't happen back-to-back in a way that blocks processing of real-time user input events (e.g. typing, scrolling, etc.)

In practice, using rIC is not about delaying things in any big or notable way. It's about changing the order at a fairly microscopic level so that the browser doesn't have to to terribly inefficient things to make things work, and so that users don't have a scroll, click or keystroke delayed when many unrelated things all want to do "this 1 quick thing". Instead, when using rIC, the browser will still make a bunch of those "quick things" happen immediately (eg <1ms), then check to see if an event has been blocked and if so handle it, and immediately go back to doing those things, etc.

Source of delay

The delay is probably not because of requestIdleCallback. The only thing rIC is doing here is change "rapidly execute all initialisation of all modules load.php just gave us" to "rapidly execute some, check for user events, rapidly execute some, check events", etc.

In fact, the requestIdleCallback shown at the top of the stack shown in the screenshots is not the call you added as part of the previous task. Reason being, that the screenshot shows execution of "ext.popups.main". But, the rIC call added was about requesting "ext.popups.main".

Elsewhere in the timeline you'll find a stack like: Fire idle callback: anonymous [ext.popups.init] > mw.loader> addScript [end of stack]. That tiny one is the one that was added.

When looking at ext.popups.* related things on a page, there are now three uses of rIC:

  1. mw.loader scheduling execution of all modules on the initial pay load in batches, one of which contains ext.popups.init.
  2. ext.popups.init requesting ext.popups.main.
  3. ext.popups.main deferring some of its DOM logic.

Number 2 is the one added.

Back to the delay:

  1. Have we checked whether this has affected Page Preview metrics, and if so, in what way were they affected? (I don't see it linked on the task, but maybe it was mentioned elsewhere?)
  1. Once we ignore the noise of each stack (naturally) starting with "Fire idle callback", on the overall level it seems the delay is mainly coming from starting the http request for "ext.popups.main" from another module, which itself also needs to be loaded first (on cold/first views). When I compare it to the network area of the timeline, it seems to align pretty much exactly. I don't see any additional delays. Within 30ms or so of "popups-init" code arriving on the network, it ends up being parsed, compiled, executing, requesting an idle callback, firing the idle callback, and making the http request from addScript for "popups-main". And within 7ms of "popups-main" it executes its payload. (This was on Chrome desktop with emulated 3GSlow and 4x CPU throttle.)

The 30ms delay is mainly because there's a bunch of other stuff in the same load.php response that takes a while for the JS engine to parse and compile before it can execute anything.