Date: 2024-03-24

Status: accepted

Context

Please read this article to understand more about the world of polyfills and writing code for browsers.

  • Concepts like polyfills and transpilation can be confusing to grasp and take into account, and always thinking “will <some statement> work on <some browser>?” over each piece of code you write is not straight-forward. But, we also don’t want customers to come to us with bugs like “I just see a white screen” (classic polyfill errors), so we need to address these issues one way or another.

    There’s an eslint module called eslint-plugin-compat which notifies us on code that might need polyfills.

  • Transpiling and syntax transformation is something vite (our chosen tool for code bundling) does out of the box. However, as they note, they do not cover polyfills. Remix has polyfill support via installGlobals, but only for specific features used by Remix themselves (fetch, Response, Request, Headers..). That means we should still handle other polyfills on our own.
  • Before we approach the actual issue of polyfills, it’s a best practise to be explicit on which browsers and versions our application supports. The most common way to do this is by specifying them in our package.json file under the browserslist key. using the value "defaults" might be a reasonable configuration for most users, however we are developing a desktop application and we’re a B2B company, so we can can be more strict about what we do support.

    To see the browser usage table used by browserslist, go to https://caniuse.com/usage-table

  • Implementing polyfills can be done in several ways:
    • Include all polyfills in our bundled code. This is a common approach, usually by using core-js, js-shims or wrappers like babel-polyfill. This is an easy approach to implement, but one that has a big cost: it can add up to 84.2 kb of minified JS to our bundle, meaning that even if your users are using an up-to-date browser, they will have to suffer the “tax” of downloading, parsing and running this javascript code, resulting in worse performance.
    • Include just the polyfills required by our code. While this is a good approach, we are still including code in our bundle that might not be necessary for the end user.
    • Dynamically deliver only the polyfills required by the user’s web browser. This can be done via services like Polyfill.io or open source modules like polyfill-library or polyfill-service. This approach is nice because the polyfill code is not bundled with our application code, which also means we can choose to cache it forever, making the user download it once and never need to download it again. Since polyfill.io has no SLA and has apparently been sold to a Chinese company, using polyfill-service seems like the best approach, and this is a good guide for it.

Decision

  • Use polyfill-library to dynamically deliver only necessary polyfills required by the user’s browser.

  • Support browsers according to this config in package.json:

    "browserslist": [
        "Chrome >= 87, Firefox >= 78, Safari >= 14, Edge >= 88",
        "not ie <= 11",
        "not and_chr < 9999, not and_ff < 9999, not ios_saf < 9999"
    ]

    To see the full list of browsers and versions, run npx browserslist

    In human-readable language, that means:

    • Explicit support for the 4 most commonly-used browsers: Chrome, Firefox, Safari, Edge
    • support the same versions that vite does when transpiling the bundle
    • Explicitly do not support IE
    • Explicitly do not support any mobile browsers (in our case, the mobile verisons of the supported browsers)
    • Implicit support for Chromium-based browsers like Opera, Brave, Arc, etc (via “Chrome” support)
  • Use eslint-plugin-compat to enforce our browserslist config

Consequences