JS Performance: State of the Art
Karim Alibhai
CTO @ HireFast
npm install karimsa
karim@hirefast.ca
Hiring on autopilot.
Disclaimer
Measure twice, cut once!
How does JS work?
karim@hirefast.ca
Write
Parse
Compile
Run
Devs write clear code
Transform code into AST
AST transforms into machine code
Machine code is executed
Lifecycle of a Program
karim@hirefast.ca
Write
Parse
Compile
Run
dev time
Compiled Languages
dev time
Interpreted Languages
Compiled vs. Interpreted Languages
karim@hirefast.ca
-
Browsers must download JS before parsing (Networks are slow)
-
Mobile CPUs are slow (Parsing takes time)
Performance Killers
karim@hirefast.ca
bitly.com/2kJ6u79
karim@hirefast.ca
Write
Parse
Compile
Run
Parse
Compile
JS engine
karim@hirefast.ca
What the heck is minification?
-
Goal: Reduce physical file size of the code
-
Rewrite JS to smaller JS with the same functionality
karim@hirefast.ca
Strategy #1: Remove whitespaces
-
Removes unnecessary whitespaces
-
Started by JSMIN back in 2003 (thanks Doug Crockford!)
karim@hirefast.ca
Strategy #1: Remove whitespaces
270 bytes
219 bytes
const mockStdout = []
const isTestEnv = process.env.NODE_ENV === 'test'
function sayHello(greeting, name) {
const message = `${greeting}, ${name}!`
if (isTestEnv) {
mockStdout.push(message)
} else {
console.log(message)
}
}
sayHello('Hello', 'world')
const mockStdout=[],isTestEnv=process.env.NODE_ENV==='test',sayHello=(greeting,name)=>{const message=`${greeting}, ${name}!`;if(isTestEnv){mockStdout.push(message)}else{console.log(message)}};sayHello('Hello', 'world')
karim@hirefast.ca
Strategy #2: Mangling
-
Rewrites variable bindings to take less bytes
-
Same example as before:
const a = []
const b = process.env.NODE_ENV === 'test'
function c(d, e) {
const f = `${d}, ${e}!`
if (b) {
a.push(f)
} else {
console.log(f)
}
}
c('Hello', 'world')
184 bytes (from 270 bytes)
karim@hirefast.ca
Strategy #3: Constant-folding
-
Replace references to constant variables with their values
-
For example:
const mockStdout = []
const isTestEnv = process.env.NODE_ENV === 'test'
function sayHello(greeting, name) {
if (isTestEnv) {
mockStdout.push(`${greeting}, ${name}!`)
} else {
console.log(`${greeting}, ${name}!`)
}
}
sayHello('Hello', 'world')
259 bytes (from 270 bytes)
karim@hirefast.ca
Strategy #3: Constant-folding
Note: some env variables can be constant
const isTestEnv = 'production' === 'test'
const isTestEnv = process.env.NODE_ENV === 'test'
const isTestEnv = false
const mockStdout = []
const isTestEnv = false
function sayHello(greeting, name) {
if (false) {
mockStdout.push(`${greeting}, ${name}!`)
} else {
console.log(`${greeting}, ${name}!`)
}
}
sayHello('Hello', 'world')
229 bytes (from 270 bytes)
karim@hirefast.ca
Strategy #4: Dead-Code Elimination
-
Removes branches that will never be executed
const mockStdout = []
function sayHello(greeting, name) {
console.log(`${greeting}, ${name}!`)
}
sayHello('Hello', 'world')
128 bytes (from 270 bytes)
function sayHello(greeting, name) {
console.log(`${greeting}, ${name}!`)
}
sayHello('Hello', 'world')
105 bytes (from 270 bytes)
karim@hirefast.ca
All together now
270 bytes
62 bytes
const mockStdout = []
const isTestEnv = process.env.NODE_ENV === 'test'
function sayHello(greeting, name) {
const message = `${greeting}, ${name}!`
if (isTestEnv) {
mockStdout.push(message)
} else {
console.log(message)
}
}
sayHello('Hello', 'world')
function a(b,c){console.log(`${b}, ${c}!`)}a('Hello','world')
28 bytes
console.log('Hello, world!')
karim@hirefast.ca
The Impact
For every 100ms decrease in homepage load speed, Mobify's customer base saw a 1.11% lift in session based conversion, amounting to an average annual revenue increase of $376,789.
Source: WPO Stats
karim@hirefast.ca
Programs should be written for people to read, and only incidentally for machines to execute.
from "Structure and Interpretation of Computer Programs"
karim@hirefast.ca
abstractions
-
Variables & Constants
-
Modules
-
Classes & Objects
-
Functions
alibhai.co/fallaciesofjsperf
karim@hirefast.ca
Challenge: How do we overcome the cost of abstraction?
karim@hirefast.ca
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.disable('etag')
app.disable('x-powered-by')
app.post('/hello', bodyParser.json(), function(req, res) {
res.json({ hello: req.body.name, query: req.query })
})
app.listen(3000, () => {
console.log('Listening :3000')
})
329 bytes
51 dependencies
714, 969 bytes bundled (~0.7 MB)
568, 675 bytes after minification (~0.6 MB) - only ~20% saved
karim@hirefast.ca
One day, optimization compilers will catch up...
karim@hirefast.ca
But what do we do today?
require('express')()
.get('/hello', (_, res) => res.json({ hello: 'world' }))
.listen(1024, () => console.log('listen :1024'))
require('http')
.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/hello') {
res.writeHead(200, {
'Content-Type': 'application/json',
})
res.end(JSON.stringify({ hello: 'world' }))
} else {
res.writeHead(404)
res.end('404 Not Found')
}
})
.listen(1024, () => console.log('listen :1024'))
we want to write this
but run this
karim@hirefast.ca
Compile-time Macros
-
Help the compiler produce more optimal code
-
Essentially just a function that gets executed at compile-time instead of run-time
-
github.com/kentcdodds/babel-plugin-macros
karim@hirefast.ca
demo
github.com/karimsa/http-macros
karim@hirefast.ca
micro-benchmarks (before)
karim@hirefast.ca
github.com/karimsa/http-macros
micro-benchmarks (after)
karim@hirefast.ca
github.com/karimsa/http-macros
other artwork
-
github.com/facebook/prepack
-
github.com/google/closure-compiler
-
github.com/glimmerjs/glimmer-vm
experiments
-
github.com/sokra/rawact
-
github.com/karimsa/iife-pop
-
github.com/karimsa/classi
karim@hirefast.ca
The future (and the past) is compilers!
karim@hirefast.ca
(Pst. Computer Science existed before JS! Not the other way around.)
karim@hirefast.ca
JS Performance: State of the Art
By Karim Alibhai
JS Performance: State of the Art
State of the art in JS performance.
- 1,617