Frontend Development 26 min read

Should You Still Transpile ES6? A Deep Dive into Browser Compatibility and Performance

This article examines how modern browsers now support most ES6 features, compares the bytecode size of native ES6 syntax versus Babel‑transpiled ES5 code across a range of language features, and explains when skipping transpilation can improve performance while still handling polyfills and runtime requirements.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Should You Still Transpile ES6? A Deep Dive into Browser Compatibility and Performance

To support legacy browsers, especially the IE series, developers often use Babel or similar tools to transpile ES6+ code down to ES5.

Six years after ES6 was released in 2015, how well do browsers actually support it? According to CanIUse data, 98.14% of browsers support ES6; the remaining gap is mainly due to the 1.08% market share of Opera Mini, which still uses the 2015 version.

On mobile, Safari on iOS and Chrome released after 2016 fully support ES6, while Safari on iOS 7‑9.3 accounts for only 0.15% of users. Android WebView has supported ES6 since version 5.

Because a tiny fraction of old devices prevents the overall usage rate from reaching 99%, the argument for mandatory transpilation loses weight, especially for mid‑to‑high‑end devices that are already recent.

However, ES6 and later versions consist of many separate features, so a simple "ES6 is better than ES5" abstraction is insufficient. Below is a feature‑by‑feature comparison of transpiled versus native code.

Better Without Transpiling

1. const

const

enforces constant checks. Example:

<code>let f1 = () => { const a = 0; a = 2; }; f1();</code>

After transpilation, Babel generates a

_readOnlyError

helper:

<code>function _readOnlyError(name) { throw new TypeError('"' + name + '" is read-only'); }
var f1 = function f1() { var a = 0; 2, _readOnlyError("a"); };
f1();</code>

Inspecting the source makes it clear which version is more efficient.

2. Array Copy

ES6 introduced the spread operator

...

for array copying:

<code>const a1 = [1,2,3,4,5,6,7,8,9,10];
let a2 = [...a1];</code>

Babel transpiles this to a

concat

call:

<code>var a1 = [1,2,3,4,5,6,7,8,9,10];
var a2 = [].concat(a1);</code>

From a bytecode perspective, the native spread uses V8’s

CreateArrayFromIterable

instruction and takes only 9 bytes, whereas the transpiled version creates a function call and an empty array, totaling 21 bytes.

<code>Bytecode length: 9
Parameter count 1
Register count 2
Frame size 16
...CreateArrayFromIterable...</code>
<code>...CreateArrayLiteral...
...CreateEmptyArrayLiteral...</code>

3. String.raw

Transpiling

String.raw

also adds a helper function:

<code>let f1 = () => { String.raw`
`; };
f1();</code>
<code>var _templateObject;
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
var f1 = function f1() { String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n"]))); };
f1();</code>

4. Symbol

Symbols are a new ES6 primitive. Native code uses

typeof s1

, but Babel must import a helper:

<code>let f2 = () => { let s1 = Symbol(); return typeof s1; };</code>
<code>function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
var f1 = function f1() { var s1 = Symbol(); return _typeof(s1); };</code>

5. Rest Parameters

V8 provides a

CreateRestParameter

instruction, but the original

arguments

uses

CreateMappedArguments

. Source code without transpilation is shorter:

<code>let f1 = (...values) => { let sum = 0; for (let v of values) { sum += v; } return sum; };
f1(1,4,9);</code>
<code>var f1 = function f1() { var sum = 0; for (var _len = arguments.length, values = new Array(_len), _key = 0; _key < _len; _key++) { values[_key] = arguments[_key]; } for (var _i = 0, _values = values; _i < _values.length; _i++) { var v = _values[_i]; sum += v; } return sum; };</code>

6. Optional catch binding

ES2019 allows omitting the error parameter in

catch

. Babel generates an unused variable:

<code>let f3 = f2 => { try { f2(); } catch { console.error("Error"); } };</code>
<code>var f1 = function f1(f2) { try { f2(); } catch (_unused) { console.error("Error"); } };</code>

The generated bytecode shows additional

CreateCatchContext

and

CATCH_SCOPE

handling when the variable is present, but not when it is omitted.

7. Generator

Using an iterator explicitly (e.g., a generator) incurs a large overhead after transpilation because Babel injects the

regeneratorRuntime

support library.

<code>let f1 = () => { let obj1 = { *[Symbol.iterator]() { yield 1; yield 2; yield 3; } }; [...obj1]; };</code>
<code>function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
... // many helper functions generated by Babel
var f1 = function f1() { var obj1 = { [Symbol.iterator]: function _callee() { return regeneratorRuntime.mark(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 1; case 2: _context.next = 4; return 2; case 4: _context.next = 6; return 3; case 6: case "end": return _context.stop(); } } }, _callee); } }; _toConsumableArray(obj1); };</code>

Running this in Node throws

ReferenceError: regeneratorRuntime is not defined

unless the runtime library is installed via

npm install --save @babel/polyfill

and required.

8. Class

Although

class

is syntactic sugar over functions, Babel adds several helper functions (

_createClass

,

_classCallCheck

,

_defineProperties

) that increase code size.

<code>class Code { constructor(source) { this.source = source; } }
code1 = new Code("test1.js");</code>
<code>function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Code = /*#__PURE__*/_createClass(function Code(source) { _classCallCheck(this, Code); this.source = source; });
code1 = new Code("test1.js");</code>

9. Polyfill‑dependent built‑ins

Features like

Set

,

Map

,

Number.isNaN

, etc., are not transpiled by Babel; they rely on runtime polyfills such as

@babel/polyfill

, which bundles

core‑js

and

regenerator-runtime

. Example usage:

<code>Array.from(new Set([1,2,3,2,1]));
[1, [2,3], [4,[5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));</code>
<code>import from from 'core-js-pure/stable/array/from';
import flat from 'core-js-pure/stable/array/flat';
import Set from 'core-js-pure/stable/set';
import Promise from 'core-js-pure/stable/promise';
from(new Set([1,2,3,2,1]));
flat([1, [2,3], [4,[5]]], 2);
Promise.resolve(32).then(x => console.log(x));</code>

Transpiling Can Be Better

1. Destructuring Assignment

Using the classic variable‑swap example:

<code>let f1 = () => { let x = 1; let y = 2; [x, y] = [y, x]; }; f1();</code>

Native transpilation produces 44 bytes of bytecode, while the destructuring version (which involves an iterator) expands to 189 bytes.

<code>... (bytecode omitted for brevity) ...</code>

Compatibility Still Needs Waiting

1. Nullish Coalescing Operator

The

??

operator returns the right‑hand side when the left is

null

or

undefined

. Native V8 bytecode uses a single

JumpIfUndefinedOrNull

instruction (9 bytes). After Babel transpilation it becomes two separate checks (

JumpIfNull

and

JumpIfUndefined

), increasing the bytecode to 15 bytes.

<code>function greet(input) { return input ?? "Hello world"; }</code>
<code>function greet(input) { return input !== null && input !== void 0 ? input : "Hello world"; }</code>

When browsers support

??

, skipping transpilation yields better performance.

2. Exponentiation Operator

V8 provides an

Exp

instruction, so

x ** x

compiles to just 6 bytes. Babel rewrites it to

Math.pow(x, x)

, which requires a function call and 16 bytes of bytecode.

<code>let f1 = x => x ** x; f1(10);</code>
<code>var f1 = function f1(x) { return Math.pow(x, x); }; f1(10);</code>

JSX

React JSX must be transpiled because browsers cannot parse it natively. The amount of generated code varies with the target environment. For iOS 9, Babel produces many helper functions for destructuring and iterator handling; targeting iOS 15 (which supports destructuring) reduces the helper code dramatically.

<code>// iOS 9 target (many helpers)
function Example() { const [count, setCount] = useState(0); return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", null, "You clicked ", count, " times"), /*#__PURE__*/React.createElement("button", { onClick: () => setCount(count + 1) }, "Click me")); }</code>
<code>// iOS 15 target (native destructuring)
function Example() { const [count, setCount] = (0, _react.useState)(0); return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", null, "You clicked ", count, " times"), /*#__PURE__*/React.createElement("button", { onClick: () => setCount(count + 1) }, "Click me")); }</code>

Conclusion

From the examples above, most features show that avoiding transpilation when the target browsers already support the native ES6 syntax leads to smaller bytecode and better V8 performance. Only features that require iterators (e.g., destructuring) or runtime polyfills incur significant overhead when transpiled. For modern mid‑to‑high‑end devices, using native ES6+ features directly is generally the more efficient choice.

performanceBabelTranspilationbrowser compatibilityES6
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.