flut1
flut1

Reputation: 295

What is the risk of global polyfills in an external script breaking the functionality of a website?

The documentation for @babel/polyfill has the following note:

If you are looking for something that won't modify globals to be used in a tool/library, checkout the transform-runtime plugin.

On the transform-runtime documentation, it says the following:

While this [@babel/polyfill usage] might be ok for an app or a command line tool, it becomes a problem if your code is a library which you intend to publish for others to use or if you can't exactly control the environment in which your code will run.

More generally speaking, a lot of articles explaining the use of polyfills say that you may want to use a different solution if you care about polluting the global namespace.

In my understanding, most polyfills are loaded conditionally. If an implementation already exists, the polyfill will not overwrite it. My question is: under what kind of circumstances can polyfills in an external script cause an existing website to break? The only reason that I have been able to find so far is that the external script might load a polyfill earlier than the code in the website itself. This may lead to problems, but when these polyfills are based on web standards, their behavior should be the same. What is the likelihood of there still being serious conflicts?

I found an interesting discussion about this on a github issue. This talks mostly about modules in the NPM ecosystem though, whereas I'm mostly interested in external scripts that facilitate things like widgets or embeds.

Any personal experience or links to discussions and articles on the subject is appreciated!

UPDATE: One of the main reasons for this question is that there were some issues with transform-runtime. With the new release of core-js and babel these issues seem to have been addressed. Regardless I am still interested in answers to the original question above.

Upvotes: 3

Views: 1665

Answers (1)

Kaiido
Kaiido

Reputation: 137131

Well, polyfills are rarely perfects, and as you say, they almost all work conditionally.

Let's say library-1 injects its own polyfill (polyfill-A) for a feature called Interface.
This polyfill-A could very well implement only a few methods of the full API of Interface, for instance, the official API could be something like

interface Interface {
  constructor(optional (Interface or DOMString) foo);
  undefined doSomething();
  undefined doSomethingElse();
};

But passing an Interface instance in the constructor may have been added only later on in the specs, or doSomethingElse may have been omitted by that polyfill, or simply not tested correctly and all these little omissions may have been fine for library-1 because they don't use any of these.
Now when library-2's own polyfill will check if there is already an Instance constructor available, it will see that yes, it's already defined, and will thus not re-implement it.
However, library-2 may need to pass an Interface in the constructor, or it may need to call its doSomethingElse() method. And when it will try to do so, the code will crash, because even though the author of library-2 did include a polyfill that does correctly implement both features, library-1's polyfill's implementation is the one running and accessible.

<script>
  // library-1.js
  (function polyfillInterface() {
    if (typeof Interface !== "function") {
      class Interface {
        constructor(foo) {
          this.foo = foo.toUpperCase();
        }
        doSomething() {
          return this.foo + "-bar";
        }
      }
      globalThis.Interface = Interface;
    }
  })();
  {
    // for library-1, everything works well
    const instance = new Interface("bla");
    console.log(instance.doSomething());
  }
</script>

<script>
  // library-2.js
  (function polyfillInterface() {
    if (typeof Interface !== "function") {
      class Interface {
        constructor(foo) {
          if (foo instanceof Interface) {
            this.foo = foo.foo;
          }
          else if (typeof foo === "string") {
            this.foo = foo.toUpperCase();            
          }
          else {
            throw new TypeError("neither an Interface nor a DOMSrting");
          }
        }
        doSomething() {
          return this.foo + "-bar";
        }
        doSomethingElse() {
          return this.foo.toLowerCase() + "-bar";
        }
      }
      globalThis.Interface = Interface;
    }
  })();
  {
    // for library-2, everything is broken
    const instance_1 = new Interface("bla");
    try {
      console.log(instance_1.doSomethingElse());
    }
    catch(err) {
      // instance_1.doSomethingElse is not a function
      console.error(err);
    }
    // TypeError: foo.toUpperCase is not a function
    const instance_2 = new Interface(instance_1);
  }
</script>

And it may be things very hard to determine, for instance Promise.then() should fire in the same event loop than they resolved (in the microtask-queue), rather than in the next one like a normal task would, and numerous Promise libraries may have been using setTimeout(fn, 0) to make the asynchronicity instead of e.g using a MutationObserver when available.

That's why when writing a library, it's good to link to polyfills, but not to include them yourself.

Upvotes: 3

Related Questions