Browser support issues in Next.js

Browser compatibility is a pain in the arse for many front-end developers. Although most modern browsers have supported the new features or syntax which mitigates the issues, the notorious IE browser is likely to be haunting around for a couple of years. Thus it is essential for us to learn some tricks to address these issues.

When I decided to use Next.js to build the website, I didn't think too much about the support of IE. So I was panic when I was assigned to tackle this challenge. Luckily, Next.js supports IE11 out of box which is a true life saver (another shout out for Next.js). Nevertheless, I still need to implement polyfill to handle specific problems. Browser compatibility comes down to two major parts -- JavaScript and CSS. I am going to elaborate both of them in two sections.

JavaScript

JavaScript compatibility takes syntax and new built-ins into account. The most essential tool to address JavaScript compatibility is Babel. Babel converts the new JavaScript code e.g. ES6+ into a backwards compatible version for older browsers or environments.

@babel/preset-env

Babel compiler has a wide range of plugins. The Babel community offers a preset environment @babel/preset-env to make our life easier. The default preset includes features in babel-preset-es2015, babel-preset-es2016 and babel-preset-es2017. If you have an array of preset environments, they will be loaded from right to left. It means you should put the most backwards preset at the right end.

the most basic .babelrc

{
  "presets": ["@babel/preset-env"]
}

Browserlist

If you want to include the polyfills and code transform for target browsers, you should then configure the browserlist. Browserlist is a very smart tool. It offers a range of semantic queries to identify the target browsers such as ">5%" ( larger than 5% global usage), "last 2 major versions", "ie 6-8" etc. You can either set the browserlist in the .babelrc or create an isolated .browserlistrc file which is shared by other config files in the project.

.babelrc

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "browsers": [">0.25%", "not ie 11", "not op_mini all"]
        }
      }
    ]
  ]
}

.browserlist

# Browsers that we support
last 1 version
> 1%
IE 10

package.json

{
  "private": true,
  "dependencies": {
    "autoprefixer": "^6.5.4"
  },
  "browserslist": ["last 1 version", "> 1%", "IE 10"]
}

@babel/polyfill

Babel only transpiles JS syntax by default except for new built-ins such as Iterator, Generator, Set etc. In my case, the new ES6 method Array.from is not supported by ES5. It throws an error in an IE11 browser. So we had to add the plug-in @babel/polyfill. What we need to do is to install the @babel/polyfill as well as core-js in the dependencies (not devDependencies). The .babelrc should be configured like this. There are two options for "useBuiltIns".

  • If "usage" is specified, then you don't need to include in either webpack.config.js entry array nor source.
  • If "entry" is specified in .babelrc then include @babel/polyfill at the top of the entry point to your application via require or import.
  • If use "false", you need to add the entry array in your webpack.config.js. In my case, the "usage" option doesn't include all modules for some reason. So I chose "entry" and import @babel/polyfill in head of the entry file.

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry"
      }
    ]
  ]
}

However, the major downside of @babel/polyfill is its huge size. It adds all available methods to the prototypes. It could be a massive waste. We can only include particular core-js module to address this issue. Another problem of @babel/polyfill is that it pollutes global scope and prototypes. If it's used in a library, it could lead to unexpected consequences to the library users.

@babel/runtime, @babel/transform-runtime

In lieu of @babel/polyfill, there is an alternative -- @babel/plugin-transform-runtime. It only generates the necessary helpers in the source which significantly reduces the size of imports. Plus, the imports don't pollute the global scope. @babel/plugin-transform-runtime is often used along with @babel/runtime which removes the duplicate helpers generated by the transform plugin.

A caveat should be noted is that instance method such as "foobar".includes("foo") will not work since that would require modification of existing built-ins. Under this circumstance, you should use babel/polyfill instead.

CSS

CSS used to be a big mess among different browsers. The notorious IE browsers are a nightmare to many developers. Besides, the inconsistent feature support made by different vendors causes a serious headache. There are various approaches to tackle this problem. I would introduce the most commonly used ways in the following.

PostCSS & Autoprefixer

One of the most popular ways to mitigate the differences of CSS rules across browsers is PostCSS. More precisely, it's the Autoprefixer plugin. Basically, the plugin parses CSS and add vendor prefixes to CSS rules. To set up PostCSS in Next.js, you need to install @zeit/next-css, postcss-loader and optionally postcss-preset-env. Then create a next.config.js and a postcss.config.js.

next.config.js

const withPlugins = require('next-compose-plugins');
const withCss = require('@zeit/next-css');

module.exports = withPlugins([
  [
    withCss,
    {
      postcssLoaderOptions: {
        parser: true,
      },
    },
  ],
]);

postcss.config.js

module.exports = {
  plugins: {
    'postcss-preset-env': {},
  },
};

You can add 'grid: true' to enable the grid layout in IE. Personally, I don't recommend it. Because grid layout in IE behaves very different than other popular browsers. The config also picks up .browserlistrc setting. You can specify it as 'browser: "> 5%"' in the config or create an isolated .browserlist file as mentioned above.

<!--[if IE]>

Property overrides

@supports

Modernizr