w3c / webextensions

Charter and administrivia for the WebExtensions Community Group (WECG)
Other
576 stars 50 forks source link

userstyles support #615

Open tophf opened 1 month ago

tophf commented 1 month ago

Currently there's no support for userstyles in Chrome and it's become a big problem in ManifestV3 as it removes persistent background scripts, meaning that the style is often applied after 100-200ms necessary to spin up the background script environment, read the database of styles, inject the CSS. It produces a random and frequent FOUC (flash of unstyled content) while opening a page. Even an occasional FOUC is unacceptable as many styles completely change the appearance of web sites, so even a 16ms glimpse of unstyled content may be painful if it's white instead of the expected dark.

Previously, using a persistent background script a ManifestV2 extension was fast enough to apply the styles consistently without FOUC in most cases using extension messaging, the rest was covered by webRequestBlocking + URL.createObjectURL + XMLHttpRequest workaround that is impossible now in Chrome (webRequestBlocking is only available in a policy-installed ManifestV3 extension and URL.createObjectURL is still not exposed to service workers, so it can't be used even in a policy-installed extension as it requires asynchronous offscreen document communication while webRequestBlocking listener can only be synchronous in Chrome).

The only workaround in Chrome's MV3 is userScripts API, but it's a heavy hammer, which has the potential of XSS if the extension doesn't properly embed the CSS into the JS code string.

Firefox has browser.contentScripts.register that supports literal CSS code.

Primary goals to achieve parity with extensions like Stylus or Stylish (millions of users, ~100K userstyles):

  1. Register/update a literal CSS code string.

    Probably in chrome.scripting.registerContentScripts/updateContentScripts using cssCode: 'string' and a mandatory id.

  2. Support id in scripting.insertCSS/removeCSS to allow the user of the extension to toggle the styles on-the-fly.

  3. Inject at document_start when loading/prerendering the page, i.e. ignore runAt.

  4. Place the styles after all the other styling mechanisms (document.styleSheets, document.adoptedStyleSheets, styles added to the DOM root after document.body) to ensure they override page selectors with the same specificity in any origin mode (author/user).

    This is not the case in Chrome, where insertCSS injects before adoptedStyleSheets last time I checked.

  5. Don't apply CSP of the page to data: inside the style, ideally to any URL for which the extension has a host permission.

    These are used for custom backgrounds/icons.

    If any URL exemption is undesirable, then consider allowing blob:chrome-extension://.... so that the extension can replace the external links in CSS with URLs to blobs it downloaded when installing the style.

  6. Match the #-hash part of URLs.

    Many styles augment SPA sites with hash-fragment routing. Currently extension matching patterns completely ignore it. May be an optional mode.

  7. Add a method to query the matched ids for tabId/frameId/documentId so that the extension can show the number in its icon's badge and display the names of the styles in its popup, which allows the user to manager the styles (edit, configure, remove, toggle, reorder).

  8. Support JS regexp matches/excludes or at least RE2.

    This is a primary goal because unlike a userscript, a userstyle is not JS so it can't perform regex matching. An extension can't do it separately just for such styles, because it will be too late in many cases, i.e. FOUC.

    Many userstyles use regexps as it's the only way to avoid targeting the wrong page that has the same CSS selectors inside.

    If this is not implemented, extensions need a way to set the order in scripting.insertCSS e.g. before: 'id', because users often have several/many styles applied to a page and the order is important in CSS.

Secondary goals:

  1. Automatically apply the matching userstyles and remove the no-longer-matching userstyles at a URL change due to a soft navigation like history.pushState/replaceState, hash-navigation due to a click on a link, back/forward navigation.

    Although this can be done by an extension, but it'll have to read all the styles and match them manually. Styles often have a lot of matching patterns, many of them regexps.

  2. Allow re-ordering of the styles similarly to #606 by specifying before: 'id' or priority when registering/updating.

Alternative solution

536 is the bare minimum to solve FOUC, but still wasteful due to the lack of full regexp matches used by many popular styles, so megabytes of CSS would be injected into every tab/frame and 1-100 regexp expressions (many styles have dozens) would be re-compiled and re-executed. It might be even better to extend chrome.declarativeContent with SetContentData({....}) because it's more flexible and already supports regexp (RE2).

erosman commented 1 month ago

See also: Proposal: Re-evaluate userScript and userStyle API

tophf commented 1 month ago

536 is the bare minimum to solve FOUC, but still wasteful due to the lack of full regexp matches used by many popular styles, so megabytes of CSS would be injected into every tab/frame and 1-100 regexp expressions (many styles have dozens) would be re-compiled and re-executed. It might be even better to extend chrome.declarativeContent with SetContentData({....}) because it's more flexible and already supports regexp (RE2).