Why you should use SWC (and not Babel)

7 min read 2193

What is Babel?

Babel is a tool to help us transpile newer versions of JavaScript code such as ES6 into older versions of JavaScript — it even helps you transpile TypeScript.

Babel reads the source code based on the configs you define for it and compiles newer JavaScript features such as arrow functions or optional chaining. This happens with Babel’s three major tools:

  • First, Babel’s parser takes the JavaScript code and converts it to an Abstract Syntax Tree (AST) which is the structure of the source code understandable by the computer
  • Next, Babel’s traverser takes the AST, explores it and modify it to the intended code we defined in our Babel configs
  • Lastly, Babel’s generator will translate the modified AST back to the regular code
abstract syntax tree
Source: https://www.sitepoint.com/understanding-asts-building-babel-plugin/

Babel’s alternative (written in Rust)

SWC is also a transpiler for JavaScript, it is written in Rust and is much faster than Babel. Rust is known for its performance and reliability and has encouraged many businesses to rewrite partial or full parts of their code with it. For example:

One of the reasons Rust is so performant is its way of handling garbage collection which is a memory management approach to free up memory resources by data objects that no longer have use for them. Since Rust decides which memory resources are no longer necessary at compile time and does not have to run continuously, processing time decreases while performance gains increase.

As we all know, code transpiling is an expensive process and that’s why having a transpiler written in Rust can be much more performant. We are gonna explore this further, but first, we should determine if we need a transpiler:

Why do we even need a transpiler?

There are cases where the usage of a transpiler might not be necessary:

  • If you are building a simple project that mostly relies on a well-supported version of JavaScript like ES3. For example, running this code will work on almost all of the browsers, so if your usage of JavaScript is mostly like this, you should be fine without a transpiler:
// we print it, but we don't agree with it
function saySomething (something) {
    console.log(`${something}. But don't tell anyone.`);
}

saySomething("I don't like Javascript!");
  • If you are building a simple project that relies on newer versions of JavaScript like arrow functions, but the browsers you need to support also do support those new features. For example, running the below code in a newer version of Chrome (45+) should be fine:

can I use arrow functions

// we print it, but we don't agree with it
const saySomething = something => {
  console.log(`${something}. But don't tell anyone.`);
};

saySomething("I don't like Javascript!");

Other than these cases, the need for a transpiler in an application is necessary. Browsers use different types of JavaScript engines like V8 (Chrome), SpiderMonkey (Firefox), and Chakra (IE). This means that even with a standard JavaScript specification, the timing of getting standards in different browsers and the level of their support is varied extensively.

That is why we need a consistent handling of our JavaScript code across many different browsers, without the worry of breaking something or losing the chance of using new features.

Our reliance on transpilers is not only limited to converting ES6 or TypeScript to ES5; transpilers bring the future of JavaScript to us today and let us handle many cases of JavaScript conversion like ES2019. This is a very powerful tool for today’s JavaScript developers.

So we have established why we need transpilers. Now it is time to test SWC usage with a simple setup and later compare its relative performance and speed to Babel.

SWC usage

SWC can be installed as a package from NPM package manager.

First, start running this command in the root of your directory:

// use `cd` to go to the right directory and then run
mkdir swc_project

// initialize a package.json
npm init

// install swc core as well as its cli tool
npm install --save-dev @swc/core @swc/cli

By running this, we now have both the SWC core as well as the CLI. The core package will help us in our build setup, while the CLI package can be run with a command in the terminal.

As a first step, we focus on the CLI tool to transpile our JavaScript files. Imagining we have the below JavaScript file which exists in the root of directory:

//  async.js
const fetch = require("node-fetch");

async function getData() {
    let res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    let json = await res.json();
    console.log('data', json);
}

getData();
// result:
// ▶Object {userId: 1, id: 1, title: "delectus aut autem", completed: false}

You can run the below commands for transpilation against this file like this:

// running this command will emit the transpiled data to stdout
// and it will be printed out in the terminal
npx swc async.js

// running this will create a new file called `output.js`
// with the transpiled data
npx swc async.js -o output.js

// running this will create a new directory called `transpiledDir`
// and will transpile all th files in the original dir
npx swc src -d transpiledDir

Note — to see what the transpiled file looks like, you can use this SWC playground.

Now as a second step, we want to include SWC as a tool in our build system. For this one, we want to use Webpack as a more advanced and configurable builder.

For starters, let’s see how our package.json would look for a setup of Webpack and SWC. With this setup, we can run npm run-script build to let webpack build our package; additionally, we can run npm run-script start to let Webpack serve our application:

{
  "name": "swc-project",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rm -rf ./dist && webpack",
    "start": "webpack-dev-server"
  },
  "license": "MIT",
  "devDependencies": {
    "@swc/core": "^1.1.39",
    "css-loader": "^3.4.0",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.1.1",
    "swc-loader": "^0.1.9",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  },
  "dependencies": {
    "node-fetch": "2.6.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "regenerator-runtime": "^0.13.5"
  }
}

Our above configuration for building and starting an application is stored in a webpack.config.js file, which will automatically be picked up by Webpack. There are a few things going on in this file:

  • output: We are setting the name and location for Webpack to output your bundles, assets, and files including all the transpiled files
  • devServer: We are serving our Webpack app through this config by telling Webpack where to serve the content from as well as defining a port to listen for requests on
  • HTMLWebpackPlugin: We are defining this plugin to make the process of serving our HTML file with Webpack bundles included easier

But the most important part of this config is the swc-loader, which allows us to transpile JavaScript files with .js or .jsx file extensions:

// global dependencies
const path = require('path');
const HTMLWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  },
  module: {
    rules: [
        {
        test: /\.jsx?$/ ,
        exclude: /(node_modules|bower_components)/,
        use: {
            // `.swcrc` in the root can be used to configure swc
            loader: "swc-loader"
        }
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: "html-loader",
            options: { minimize: true }
          }
        ]
      },
      {
        test: /\.scss/i,
        use: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  plugins: [
    new HTMLWebpackPlugin({
      filename: "./index.html",
      template: path.join(__dirname, 'public/index.html')
    })
  ]
};

With swc-loader set up in our Webpack config, we have come halfway for transpiling our JavaScript files. However, we still need to instruct SWC on how to transpile our files. It turns out, SWC has a similar approach to Babel by defining a config file in the root directory called .swcrc . Let’s see what this config looks like for a project that wants to transpile TypeScript.

In this config, we are using the test config as a Regex to only match with files that have a .ts file extension. Additionally, with the jsx.parser config, we are instructing SWC which parser to use for the transpilation (could be typescript / ecmascript).

However, we still have more control over the syntax parsing by defining which options of the transpilation are intended for our use case. For example, in this example, we are interested in transpiling Typescript decorators and dynamic imports, but ignore transpiling files with .tsx file extension:

// .swcrc

{
  "test": ".*.ts$",
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true,
      "dynamicImport": true
    }
  }
}

Now, let’s imagine that we want to use React in our webpack SWC example above. As we know, in React we can use a certain file extension called .jsx for writing React components:

// App.jsx

// global dependencies
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return <h1>My SWC App</h1>;
};

ReactDOM.render(<App />, document.querySelector("#root"));

Serving this file through Webpack needs the correct webpack loader which we already have and defined above. It also requires the right transpilation settings in .swcrc file. Now with this approach, we are using the latest features of modern JavaScript (ES2019) as well as supporting .jsx file when transpiling. Additionally, if we need extra transpilation settings for our React project, we have plenty of settings at hand:

// .swcrc

{
    "jsc": {
      "parser": {
        "syntax": "ecmascript",
        "jsx": true
      }
    }
  }

Speed comparison between Babel and SWC

As we discussed before, the speed of a transpiler is critical since it is baked into the build process, and, for many developers, any time that can be saved in this area is precious. Let’s see how these two tools compare in terms of speed.

First, we compare them in an artificial way and that is running code transformation for Babel and SWC in a synchronous manner. As we know, JavaScript is single-threaded and it would be impossible to run heavy computations in an asynchronous way in a real-life application. But this would still give us an indicator of the speed comparison. Let’s see these benchmark comparisons run on a single core CPU (tests performed by the maintainer of SWC project):

Transform Speed (operation/second) Sample Runs
SWC (ES3) 616 ops/sec 88
Babel (ES5) 34.05 ops/sec 58

This indicates that even though with a more expensive process of ES3 transformation for SWC, the speed of SWC transpilation is evident compared to Babel.

Now, if we want to benchmark a more realistic scenario, we can run samples against await Promise.all(), which is a more expensive and real scenario for handling the operations in JavaScript. With this benchmark, the number of CPU cores and parallel computations come into play. In another benchmark that was run, two experiments were done. Both used a computer with 8 CPU cores with a parallelism of 4.

The first experiment ran with 4 promises:

Transform Speed (operation/second) Sample runs
SWC (ES3) 1704 ops/sec 73
Babel (ES5) 27.28 ops/sec 40

The second experiment ran with 100 promises:

Transform Speed (operation/second) Sample runs
SWC (ES3) 2199 ops/sec 54
Babel (ES5) 32 ops/sec 6

Note — if you are interested in running the tests yourself and compare these benchmarks, you can clone this repository and then run the following commands in terminal:

// clone and cd into the cloned repository
cd node-swc

// Node.js benchmark runner, modelled after Mocha and bencha, based on Benchmark.js.
npm i benchr -g

// run the multicore.js or any other benchmarks under /benches directory
benchr ./benches/multicore.js

The main thing we can spot in these numbers is that the performance of Babel is dropping in async operations as Babel works on an event loop. This is in contrast to SWC which runs on a worker thread and can nicely scale up with the number of CPU cores.

In general, we see a clear speed gap between the two tools, as SWC tends to be around 20 times faster than Babel on a single thread and CPU core basis while being around 60 times faster in a multi-core async operation process.

Conclusion

We have covered the basics of transpilers in this article together and compared two of the JavaScript transpilers based on setup, execution, and speed perspective. By doing this, we have learned that:

  • Their setup for the build workflow are similar
  • However, SWC has a significant speed advantage compared to Babel

So if you are using Babel and thinking about transitioning to gain faster build time, make sure to:

Having said that, the idea behind SWC sounds promising and we will all keep an eye on its progress.

Resources

 

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ 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>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now

Leave a Reply