Street View performance in Firefox on Linux

Date: 2025-01-02

Google Street View supports several rendering methods: WebGL and 2D canvas. The 2D canvas method is significantly slower.

Even though WebGL is widely supported, Street View explicitly opts out of using it for Firefox on Linux. This makes GeoGuessr extremely sluggish! My good-faith guess is that it's a leftover from some years-old bug that is no longer relevant.

Street View detects this when the Maps JS SDK is first loaded, by checking the user agent string. If it detects Firefox and Linux, WebGL is disabled.

We can fix this with a userscript, by temporarily replacing the user agent string while the SDK is loading.

Install

To install a userscript, you need a userscript manager like Tampermonkey.

Once you have that, visit https://reanna.neocities.org/sv-linux.user.js to install the fix.

Although the issue exists in all Street View Embeds, the script only applies to GeoGuessr. map-making.app has already included this fix for several months. On other sites, it's probably not a big problem when the embed performs poorly.

How it works

The user agent string is on navigator.userAgent. navigator is a builtin object so it's locked down a bit. The userAgent property can't be written to. Such properties can usually still be overwritten with Object.defineProperty.

In our case, userAgent is actually defined on the navigator's prototype, so we can grab it and define a new navigator.userAgent getter that returns the original user agent, without the string "Linux":

function replaceUserAgent () {
  // Grab the property descriptor for `navigator.userAgent`
  const ua = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), 'userAgent')
  Object.defineProperty(navigator, 'userAgent', {
    get: () => {
      return (ua.value ?? ua.get.call(navigator)).replace(/Linux/, '')
    },
    configurable: true, // so we can delete it later
  })
}

To undo our overwrite, we can just delete the property from the navigator object, so subsequent uses of navigator.userAgent will walk the prototype chain and get to the original userAgent property as normal.

We can add our overwrite when the Maps SDK script tag is added to the document, and undo it once it's done:

// `script` is the Google Maps <script> element.
function hookScriptLoad (script) {
  replaceUserAgent()
  script.addEventListener('load', () => {
    delete navigator.userAgent
  }, { once: true })
}

Finally, we can detect when the script tag is added using a MutationObserver, searching all added nodes until we find one with the Maps SDK URL:

function grabGoogleScript (mutations) {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node && node instanceof HTMLScriptElement && node.src && node.src.startsWith('https://maps.googleapis.com/')) {
        return node
      }
    }
  }
  return null
}

new MutationObserver((mutations, observer) => {
  const googleScript = grabGoogleScript(mutations)
  if (googleScript) {
    hookScriptLoad(googleScript)
    // Stop listening for changes when we find the script
    observer.disconnect()
  }
}).observe(document.head, { childList: true })

And that pulls it all together.

Is this really necessary? Can't we just write replaceUserAgent() and be done with it?

That would realistically be fine. It's just nice to clean up after yourself :)