In 2020, frontend developers are still wasting a lot of time with excessive tooling. Babel is seen by some as a necessity, but I aim to show you that it’s not.
By the end of this article, you will know:
Babel is a compiler that converts your modern JavaScript to run in older browsers. It can also perform other jobs such as converting JSX syntax, but it is not the only tool for that.
As browsers evolve, new APIs and ECMAScript features are added. Different browsers evolve at different speeds and prioritize different features. This leaves us with a tricky problem to solve: how can we support them all and still use modern features? Some will be incompatible.
A common solution is to write using the latest features and transpile down to older-style code the browser will understand. Transpiling describes a specialized type of compilation. It has different meanings in different contexts. In our case, there are also two separate parts to transpiling.
Transpiling is the process of converting newer language syntax that old browsers can’t understand into the old syntax they recognize.
Here is an example of transpiling the let
statement:
// the new syntax `let` was added in ECMAScript 2015 aka ES6 let x = 11; // `let` transpiles to the old syntax `var` if your transpiler target was ES5 var x = 11;
Polyfilling is the process of adding the missing methods, properties, or APIs to the old browsers by supplying your own version of the missing native code.
It can be thought of as filling in the gaps. For example, here is a polyfill for isNaN
:
// check if the method `isNaN` exists on the standard built-in `Number` object if (!Number.isNaN) { // if not we add our own version of the native method newer browsers provide Number.isNaN = function isNaN(x) { return x !== x; }; }
The best place to get polyfils is via core-js.
Transpiling and polyfilling are sometimes impossible and can add a lot of code bloat, it’s best to avoid them altogether if possible. That is why the first alternative we’re going to discuss is the best one.
If users would just upgrade their browsers, we could avoid the hassle of transpiling and they could enjoy the enhanced features and performance of the new browser. Unfortunately, things aren’t that simple.
The main culprit is large corporations having to support legacy software. The classic example is Internet Explorer, which has been a blight on web development from the beginning.
That said, things have improved a lot in recent years. Now, most browsers are evergreen, meaning they’re constantly updated. Microsoft now promotes its evergreen Edge browser, which, conveniently, uses the same V8 engine as Chrome, meaning one fewer engine to support.
To determine whether you have to support a particular browser, ask yourself the following questions.
If you already have a website or app that serves the same customer base, you can get this information from the analytics software. Below are some recent statistics from a UK-based sound engineering website I’m managing. If it were a JavaScript application with the same customers demographic, I would assume they would be using the same browsers.
If you don’t have analytic software installed, you won’t know which browsers you need to support. You’ll have to make an educated guess. If you have corporate customers, it’s far more likely you’ll need to support IE11 than if you’re marketing to web-literate technology fans.
When you support a browser, you make a commitment. Do you really want to have additional tests on every release, additional build tooling to configure, and extra dependencies to babysit?
There must be a clear financial reason to go to through all this bother. Will losing those customers who can’t access your site or app cost more than it would to support the browser?
Using modern language features and browser APIs makes writing code easier, faster, and more fun. It also makes your code more maintainable.
If you’re happy writing ES5 and using XMLHttpRequest()
, you definitely don’t need Babel, but you might need some kind of therapy.
This data is available on via Can I use, but it’s a waste of time to manually look it up. Now that you know the names of the browsers you want to support, looking up compatible features can be automated with the awesome Browserlist
application (more on this in the next section).
You can avoid the entire transpiling process altogether and instead allow your code editor to alert you if you’re using any features that are too modern for your customers’ browsers. This is the simplest option because it:
If there’s a modern feature you can’t live without, you can manually polyfill it. Otherwise, you can just use the older syntax when needed.
Before we can break down the pros and cons, we need to confirm that our Babel alternatives can do the same basic job. Let’s create a small test.
Below is the modern code we want our target environment to support once transpiled.
After the transportation, there’s a console.assert
for each function to verify it’s working as intended. In the case of eslint-plugin-compat
we’ll instead check that the incompatible code is being flagged in the linting.
test.js
// test nullish coalescing - return right side when left side null or undefined const x = null ?? "default string"; console.assert(x === "default string"); const y = 0 ?? 42; console.assert(y === 0); // test optional chaining - return undefined on non existent property or method const adventurer = { name: "Alice", cat: { name: "Dinah", }, }; const dogName = adventurer.dog?.name; console.assert(dogName === undefined); console.assert(adventurer.someNonExistentMethod?.() === undefined); // use browser API fetch, to check linting fetch("https://jsonplaceholder.typicode.com/todos/1") .then((response) => response.json()) .then((json) => console.log(json));
env
property with eslint-plugin-compatWe need a workaround for linting both language features and browser APIs together.
You can use eslint to check for the language syntax. To do that, change the env
property down from es2020
.
To check browser API compatibility, use eslint-plugin-compat
. It uses the same Browserlist config used by Babel and other tools.
Full instruction can be found in the eslint-plugin-compat repo. We’ll use the browserlist defaults
preset to use the default settings. Replace this with your own selection based on your analytics.
Browserlist automatically selects a list of browsers based on various criteria you give it.
Have a look at the list of browsers supported by the defaults
setting for browserlist. defaults
is a shortcut for:
Head to GitHub for the full list of queries available to choose your supported browsers.
Add the following packages to your project.
npm install --save-dev eslint eslint-plugin-compat
Add the following to package.json
.
"browserslist": [ "defaults" ]
Create the following .eslintrc.json
file or add these settings to your current one.
{ "extends": ["plugin:compat/recommended"], "env": { "browser": true, "es2020": true } }
Make sure you have the VS Code ESLint extension installed
Now any browser API that is incompatible with the browserlist
config in your package.json
is shown as a linting error. You can separately control which version of ECMAScript you want to support using the env
property in the .eslintrc.json
file.
It would be nice if the eslint-plugin-compat
automatically linted the language features as well, but this is currently an open issue.
With the IE 11
setting selected —
— our fetch()
API is flagged.
Change the env
property to es6
.
You’ll immediately see an error trying to use nullish coalescing
, which was released as part of Es2020.
Before we look at alternatives, let’s quickly review how to use Babel.
First, create a mini-project directory and install the dependencies we need.
mkdir babel-test cd babel-test npm init -y mkdir src dist npm install --save-dev @babel/core @babel/cli @babel/preset-env npm install --save @babel/polyfill
Add the following to your package.json
.
"browserslist": "defaults",
Write the test.js
file into src
, and then issue the following command.
npx babel src --out-dir dist --presets=@babel/env
Finally, run the file to check that the tests still work.
node dist/test.js
There should be no assertion errors, but it will say fetch is not defined
since Node.js has no fetch()
method. Here is the resulting transpiled code. Observe all the extra cruft and bloat added.
"use strict"; var _ref, _, _adventurer$dog, _adventurer$someNonEx; // test nullish coalescing - return right side when left side null or undefined var x = (_ref = null) !== null && _ref !== void 0 ? _ref : "default string"; console.assert(x === "default string"); var y = (_ = 0) !== null && _ !== void 0 ? _ : 42; console.assert(y === 0); // test optional chaining - return undefined on non existent property or method var adventurer = { name: "Alice", cat: { name: "Dinah", }, }; var dogName = (_adventurer$dog = adventurer.dog) === null || _adventurer$dog === void 0 ? void 0 : _adventurer$dog.name; console.assert(dogName === undefined); console.assert( ((_adventurer$someNonEx = adventurer.someNonExistentMethod) === null || _adventurer$someNonEx === void 0 ? void 0 : _adventurer$someNonEx.call(adventurer)) === undefined, ); // use browser API fetch, to check linting fetch("https://jsonplaceholder.typicode.com/todos/1") .then(function (response) { return response.json(); }) .then(function (json) { return console.log(json); });
Pros:
Cons:
du -sh
find . -type f | wc -l
swc is a new competitor to Babel. It is written in Rust and up to 20 times faster. This can be very important if you find yourself waiting a long time to build your project.
To set it up:
mkdir swc-test cd swc-test npm init -y mkdir src dist npm install --save-dev @swc/cli @swc/core browserslist
Add the following to your package.json
.
"browserslist": "defaults",
Write the .swcrc
config file into the project root.
{ "env": { "coreJs": 3 }, "jsc": { "parser": { "syntax": "ecmascript" } } }
Write your test file into src
, then issue the following command to transpile.
npx swc src -d dist
Run the resulting file to check that the tests still work.
node dist/test.js
The resulting swc-transpiled file looks like this:
var ref, ref1; var ref2; // test nullish coalescing - return right side when left side null or undefined var x = (ref2 = null) !== null && ref2 !== void 0 ? ref2 : "default string"; console.assert(x === "default string"); var ref3; var y = (ref3 = 0) !== null && ref3 !== void 0 ? ref3 : 42; console.assert(y === 0); // test optional chaining - return undefined on non existent property or method var adventurer = { name: "Alice", cat: { name: "Dinah", }, }; var dogName = (ref = adventurer.dog) === null || ref === void 0 ? void 0 : ref.name; console.assert(dogName === undefined); console.assert( ((ref1 = adventurer.someNonExistentMethod) === null || ref1 === void 0 ? void 0 : ref1.call(ref1)) === undefined, ); // use browser API fetch, to check linting fetch("https://jsonplaceholder.typicode.com/todos/1") .then(function (response) { return response.json(); }) .then(function (json) { return console.log(json); });
Pros:
Cons:
I did not include Google Closure Compiler as an option because it’s notoriously complicated to use. That said, it can do a good job of transpiling and polyfilling. If you have some spare time on your hands, I recommend you check it out — especially if you value small file sizes since its built-in minification is demonstrably superior.
You can also use TypeScript to transpile and core-js to manually polyfill, but this is a clumsy solution that could easily create more problems than it solves.
You don’t automatically need to support old browsers. It’s important to first look at your analytics data to see which browsers your customers are actually using.
If necessary, you can use linting to ensure backward compatibility. This will save you the hassle of creating a special build step and relying on transpilation.
If you do opt for automatic translating, then SWC is much faster than Babel and contains far fewer dependencies. There is also the option to use Google Closure Compiler or TypeScript, but these will require a bit more work to configure.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
2 Replies to "Why you don’t need Babel"
Nice article, thnx!
I’d suggest also an option: use TypeScript with target=ES5. It adds a lot of benefits.
Hm, wouldn’t it be better to live-transpile while coding? I mean clone the file into separate folder while working on it via filewatcher or similar.
I’d personally perefer live-transpiling my code while working on it instead of polyfilling every missing function. e.g.:
“npx babel –watch src –out-dir . –presets react-app/prod”