Page MenuHomePhabricator

Add skipFunction support for gadgets
Closed, DeclinedPublicFeature

Description

Feature summary
The skipFunction feature available for RL modules in extensions would be useful for gadgets as well.

Use case(s)
On enwiki, there are many hidden "loader" gadgets which merely check if another "core" gadget needs to be loaded. Such as:

There are likely more examples from other wikis. Many of these loader gadgets could be made skipFunctions of the core gadgets, reducing the size of the module manifest.

Some more complicated loaders which rely on user.options module and conditionally load different gadgets also exist (eg. Gadget-XFDcloser.js and Gadget-refToolbar.js) though these likely can't be done with skipFunction.

Implementation
For MediaWikiGadgetDefinitionRepo, skip functions could be saved as MediaWiki:Gadgets-skip-function/<name> (to match MediaWiki:Gadgets-defintion) in js contentmodel.
For GadgetDefinitionNamespaceRepo, the skip function page name (in Gadget namespace) could be specified in the json config.

Event Timeline

I think this may be confusing skip functions with conditional loading. The two are similar but different significantly with regards to dependency management.

When a module provides an interface (such as es6-promise.js, which defines a polyfill for Promise), then if the browser supports this natively we can skip loading of the module script file and still consider the module itself as "logically" fully loaded, because Promise is already defined. This is an optimisation only. It does not alter the end result, and does not alter dependency relations. If module B depends on or lazy-loads module A, then module A must be fully loaded. If its contents are redundant, it can be "skipped" which module B will not notice.

I think what you're talking about is conditional loading. For example, to load a gadget only on the edit page, or only in a certain namespace, or for articles in a certain category. In those cases, the gadget would not be considered "loaded" in the other places, and must still be possible to load anywhere as-needed, especially if the context changes at run-time.

The task for conditionally loaded gadgets is T63007: Allow specifying when a gadget should load (action, namespace, content model).

So my idea may be more hacky than I'd realised and different from the purpose of skipFunction. I was expecting such a feature could be used by gadgets which are applications, not libraries. They wouldn't be depended on by other gadgets, either via gadgets-definition or by mw.loader.using(). So arguably the dependency management isn't important for them.

T63007 can indeed cover a lot of conditional loading scenarios, but it doesn't allow JS code to be used as conditions.

I'm declining this in favour of T63007. This request is essentially for the same use case as T63007. I'd rather not maintain, test, document/explain to gadget authors multiple ways of doing the same thing; and of the two the approach described here would imho be unsuitable for a number of reasons.

Firstly, as described above, the skipFunction works by satisfying a dependency as loaded, which would be a lie in this case. This would lead to confusion when debugging production issues, and complicate the documentation around this feature, and conflate its meaning toward new developers.

Secondly, the performance footprint of the two approaches matters. The skipFunction feature works by serializing the skip code and downloading it in web browsers on every page view as part of the startup registry. This avoids additional requests and roundtrip delays, to make the polyfill satisfied as quickly as possible (instead of requiring multiple back-and-forths, first to obtain the function, then to load the script; thats 2 roundtrips, whereas today we have 0 in the common case).

This additional download of a few bytes was found to be an optimal balance, given that it is used for polyfills that are in high demand, and thus the trade-off of loading a little bit of code everywhere is worth it, given that: 1) we only have a few polyfills at any given moment, 2) it means we never load the polyfill in most browsers, and 3) core developers know to function must be kept small and efficient in its computation.

Using the skip function to transfer arbitrary user scripts would make this a very high-risk area in terms of performance, and would likely require other akward measures as well - such as artificially restricting the size of Gadget-skip-foo.js pages to be under 50 characters and contain no function calls (or something like that).

How this does differ from T63007? Well, that task would be approached from the page rendering perspective. The server side, in the same area where we check whether the gadget is enabled and allowed, we can also check these conditions and decide whether to load it. This has significant benefits:

  • The logic accurately reflect what it is meant to do: It alters when to load the gadget module by default on a page (instead of always loading it and sometimes loading it as an empty module).
  • It means other gadgets, site scripts and user scripts can depend on it or lazy-load it on other cases. Including via withgadget= per T29766, which would break in combination with a skip function.
  • It means the conditions can long and and somewhat complicated, given that we don't need to transfer it to the client-side.

I agree that this means the conditions will be limited to the kinds of checks the server supports, and that limits capabilities, but I think that's neccecary in this case. I'm happy to help figure out a way to fit any kind of conditional needs within the capabilities that we have, or help determine what new capabilities we could add to make those possible. Sometimes freedom is good, but not always. The fact that gadgets can do anything is probably a good thing, but when it comes to how they integrate and load, some restrictions are helpful. Especially noting that we do already have the capability to bypass it entirely and use any kind of loading condition in an imported user script, site script, or init-gadget for exceptional cases.