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.
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
constenforces constant checks. Example:
<code>let f1 = () => { const a = 0; a = 2; }; f1();</code>After transpilation, Babel generates a
_readOnlyErrorhelper:
<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
concatcall:
<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
CreateArrayFromIterableinstruction 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.rawalso 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
CreateRestParameterinstruction, but the original
argumentsuses
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
CreateCatchContextand
CATCH_SCOPEhandling 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
regeneratorRuntimesupport 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 definedunless the runtime library is installed via
npm install --save @babel/polyfilland required.
8. Class
Although
classis 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‑jsand
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
nullor
undefined. Native V8 bytecode uses a single
JumpIfUndefinedOrNullinstruction (9 bytes). After Babel transpilation it becomes two separate checks (
JumpIfNulland
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
Expinstruction, so
x ** xcompiles 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.