JS does *not* offer zero-cost abstractions!
Arpad “Swatinem” Borsos
Disclaimers
We have four major engines
v8 (Google)
SpiderMonkey (Mozilla)
jsc [javascript core] (Apple)
Chakra (Microsoft) [now open source, yay! \\o//]
+ some less widely known ones
Different
Optimizations,
Performance Characteristics
All code will be “modern JS” (ES6, ES2015, whatever you want to call it)
There will be no numbers, I’m sorry.
Zero cost?
JS is
just-in-time compiled
very dynamic
Rule of thumb:
if it looks like a function call
it is a bloody function call!
nothing is less than something
or
doing no work is better that doing little work
developer
productivity
runtime
performance
or
Example 1: private members
function MyClass(a) {
return {
getA() { return a; }
};
}
// this creates a fresh function object every time!
const a = new MyClass(1), b = new MyClass(1);
a.getA !== b.getA;
// also, things are *impossible* to debug / inspect
Example 1: private members
const _a = Symbol("a");
class MyClass() {
constructor(a) {
this[_a] = a;
}
getA() {
return this[_a];
}
}
// method is shared using the prototype chain
// you can still inspect privates in devtools
// BUT! prototype lookup
Example 2: closures
function asyncAction(payload) {
return (dispatch, getState) => {
setTimeout(() => dispatch({
type: "SOME_ACTION",
payload,
}), getState().delay);
};
}
// This allocates new function objects every time!
Example 2: closures
function asyncAction(payload) {
return (dispatch, getState) => { // here
setTimeout(() => dispatch({ // and here!
type: "SOME_ACTION",
payload,
}), getState().delay);
};
}
asyncAction(1) !== asyncAction(2);
// nothing we can really do about it,
// in this case at least
Example 3: Promises
fetch("foo")
.then(req => req.json())
.then(json => {
doSomeProcessing(json);
});
// Promises are awesome!
// (apart from being uncancelable, which sucks :-(
Example 3: Promises
const p1 = fetch("foo");
const p2 = p1.then(req => req.json());
const p3 = p2.then(json => {
doSomeProcessing(json);
});
// .then allocates intermediate promise objects
p1 !== p2 && p2 !== p3
Example 3: Promises
const toJSON = req => req.json();
// later inside a function:
fetch("foo")
.then(toJSON)
.then(doSomeProcessing);
// arrow functions are awesome!
// they are lightweight to type <3
// but they have a runtime cost
Example 4: JSX
const Component = ({onClick, children}) => (
<h1 onClick={() => onClick()}>
<span>Oh hi!</span>
{children}
</h1>
);
// "declarative" templates ?
Example 4: JSX
const Component = ({onClick, children}) => {
return React.createElement("h1",
// call, allocates new object
{ // allocates new object
onClick: () => onClick(),
// allocates new function object
},
React.createElement("span", null, "Oh hi!"),
// call, allocates new object
children);
};
Example 4.1: Alternatives?
// from: https://github.com/maxogden/yo-yo
const list = items => yo`<ul>
${items.map(item => yo`<li>${item}</li>`)}
</ul>`;
// tagged template strings are function calls
// also, template strings are *strings*!!!
// do you really want to parse strings
// *every single time*?
// also that particular library uses *real*
// dom elements to do the diffing *facepalm*
Example 4: JSX
compilers can help, for example by moving constant elements like the `<span>` out of the function, to avoid calling and allocating every time
avoid inline arrow functions, or inline `.bind()`
move binding to the constructor if possible, sadly not for stateless function components :-(
Example 5: Meta programming
// from https://github.com/paldepind/union-type/
const Action = Type({
Up: [], Right: [], Down: [], Left: []});
const advancePlayer = (action, player) =>
// call, allocates new object
Action.case({
// all of the below: allocate new functions
// captures arguments in closure
Up: () => ({x: player.x, y: player.y - 1}),
Right: () => ({x: player.x + 1, y: player.y}),
Down: () => ({x: player.x, y: player.y + 1}),
Left: () => ({x: player.x - 1, y: player.y}),
_: () => player,
}, action);
Example 5: Meta programming
// from https://github.com/paldepind/union-type/
// better:
// allocates objects and functions only once!
// can still have some overhead depending on impl
const advancePlayer = Action.caseOn({
Up: (player) => ({x: player.x, y: player.y - 1}),
Right: (player) => ({x: player.x + 1, y: player.y}),
Down: (player) => ({x: player.x, y: player.y + 1}),
Left: (player) => ({x: player.x - 1, y: player.y}),
_: (player) => player
});
Example 6: For-of
// yesterday
for (let i = 0; i < iterable.length; i++) {
const elem = iterable[i];
// ugly to write
}
// or
iterable.forEach(elem => {
// uses closure
// does not work with `NodeList` / `arguments`
});
// today
for (const elem of iterable) {
}
Example 6: For-of
// desugars into, roughly:
const iter = iterable[Symbol.iterator]();
while (true) {
const {elem, done} = iter.next();
if (done) { break; }
}
// awesome!
// works with `NodeList`, `arguments`
// supports custom iterators and generators
// sadly, 3-8x slower than straight for loop
// *sadface*
Exercise
[1, 2, 3, 4, 5]
.map(x => x * 2)
.filter(x => x % 2)
.reduce((a, x) => a + x, 0);
// whats the runtime cost of this code?
Conclusion
Developer productivity (convenience) is king!
Keep it simple!
Compilers can help a lot, like moving closures and constant react elements out of functions when possible
+ bundling and dead code elimination
Just wanted to raise awareness.
Know what the runtime cost of the code is that you write!
Outlook?
JS (especially asmjs and wasm) is a compiler target:
Bundlers (!!!), jsx, babel, typescript,
C++ and other LLVM languages via emscripten, etc…
Lets embrace it and do expensive things at compile time and not at run time!
a small rant
Why are JS devs so afraid of doing things at compile time?
Compile times?
# incremental dev builds with hot reloading
webpack built 1e4b27cb4e32b7bb3979 in 264ms
webpack building...
webpack built 992e6902639bbce8f527 in 2012ms
webpack building...
webpack built e113b1cb712bb9eec0f4 in 1147ms
webpack building...
webpack built 2ff58130c885d6d68090 in 2413ms
webpack building...
webpack built 68ebf18365f67cfd64d9 in 1346ms
# production build with uglify
npm run build 2> /dev/null 59,42s user 2,41s system 102% cpu 1:00,16 total
static analysis
+ another rant…
import/export modules is the best thing that happened, because they are statically analyzable
but node core developers want to force `*.mjs` on us :-(
// lib/foo.js
export default (a, b) => a + b;
// module.js
import add from "lib/foo.js"
add(1, 2);
the thing about types
TypeScript and Flow becoming more popular!
Can avoid real problems/bugs (undefined not a function), …
Can optimize code based on type guarantees…
// like rewriting
for (const elem of array) {
}
// into
for (let i = 0; i < array.length; i++) {
const elem = array[i];
}
Why isn’t anyone doing it?
what React should have been
// a compiler plugin!
// it kind of is due to jsx, but it does not go far enough!
// transform this:
const FnComponent = props => (
<h1>{props.title}</h1>
);
// into something similar to this:
class FnComponent {
constructor() {
this.elem = document.createElement('h1');
this.text = document.createTextNode();
this.elem.appendChild(this.text);
}
render(props) {
this.text.data = props.title;
}
// …
}
// not creating / comparing useless js objects all the time
// just updates the data that’s needed
// generate different code for client or SSR *at compile time*!
JS does *not* offer zero-cost abstractions
By Arpad Borsos
JS does *not* offer zero-cost abstractions
Just raising awareness about runtime cost of things you do in JS.
- 2,018