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

Made with Slides.com