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: let f1 = () => { const a = 0; a = 2; }; f1(); After transpilation, Babel generates a _readOnlyError helper:

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

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

2. Array Copy

ES6 introduced the spread operator ... for array copying:

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

Babel transpiles this to a concat call:

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

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.

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

3. String.raw

Transpiling String.raw also adds a helper function:

let f1 = () => { String.raw`
`; };
f1();
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(["
"]))); };
f1();

4. Symbol

Symbols are a new ES6 primitive. Native code uses typeof s1, but Babel must import a helper:

let f2 = () => { let s1 = Symbol(); return typeof s1; };
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); };

5. Rest Parameters

V8 provides a CreateRestParameter instruction, but the original arguments uses CreateMappedArguments. Source code without transpilation is shorter:

let f1 = (...values) => { let sum = 0; for (let v of values) { sum += v; } return sum; };
f1(1,4,9);
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; };

6. Optional catch binding

ES2019 allows omitting the error parameter in catch. Babel generates an unused variable:

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

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.

let f1 = () => { let obj1 = { *[Symbol.iterator]() { yield 1; yield 2; yield 3; } }; [...obj1]; };
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.
In 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); };

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.

class Code { constructor(source) { this.source = source; } }
code1 = new Code("test1.js");
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");

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:

Array.from(new Set([1,2,3,2,1]));
[1, [2,3], [4,[5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));
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));

Transpiling Can Be Better

1. Destructuring Assignment

Using the classic variable‑swap example:

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

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

... (bytecode omitted for brevity) ...

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.

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

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.

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

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.

// 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")); }
// 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")); }

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

babelTranspilationbrowser 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

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.