Javascript Performance

Mike Sherov

Core UX Team @ Skillshare

I am performance, and so can you!

Overview

  • We've been busy
  • Perf Prereqs
  • Common Perf Strategies
  • Perf Mechanics
  • What's Next in JS Perf?

We've been busy lately...

Very busy...

And we've achieved results.

  • 40% npm@5.3.0 vs. npm@5
  • 70% npm@5.3.0 vs. npm@4
  • 60% Webpack resolve phase
  • 50% Webpack Uglification
  • 95% Webpack Plugin Speed

Perf Prereqs

  • You can do it!
  • Everything is slow
  • No premature optimizations

You can do it!

  • Perf has some magic... But mostly, it's about focus
  • Easy to fire up a profiler. After a while, it's like any other tool
  • Most perf patches are really straightforward

Everything is slow

  • Webpack is a slow codebase. npm too
  • "Make it work. Make it right. Make it fast."
  • You think someone else made it fast already... They haven't

No premature optimizations

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%

Common Perf Strategies

  • Big O Notation - Avoid O(n^2) 
  • Caching and Memoization
  • Avoiding Garbage Collection
  • Avoid Unnecessary Immutability
  • Don't do things you don't need
  • Other JS Magic Learnings

Big O Notation - Avoid O(n^2)

  • O(1) - 1 or m ops
    • key lookup: `if (obj.keyName)`
  • O(n) - n or n*m ops
    • val in Array:`if (arr.includes(value))`
    • multi key in Object: `keys.every(key => obj[key])`
  • O(n^2) - n^2 or n^2*m ops
    • multi val in Array `arr2.every(val => arr.includes(val))`

Given n inputs, and some small constant m:

Big O Notation - Avoid O(n^2)

Why do we see a lot of n^2 in JS?

var obj1 = {a: 1}
var obj2 = {b: 2}

console.log('es5 - broken, but O(1)');

function SetAddBroken(obj, set) {
  set[obj] = true;
}

function SetHasBroken(obj, set) {
  return !!set[obj];
}

var fakeSet = {};
SetAddBroken(obj1, fakeSet);
console.log(SetHasBroken(obj1, fakeSet));
console.log(SetHasBroken(obj2, fakeSet));
console.log(fakeSet)

console.log('es5 - works, but O(n)');

function SetAddWorks(obj, set) {
  if (!set.includes(obj)) { set.push(obj); }
}

function SetHasWorks(obj, set) {
  return set.includes(obj);
}

var fakeSet2 = [];
SetAddWorks(obj1, fakeSet2);
console.log(SetHasWorks(obj1, fakeSet2));
console.log(SetHasWorks(obj2, fakeSet2));

console.log('es6 - works, and O(1)');

var set = new Set();
set.add(obj1);
console.log(set.has(obj1));
console.log(set.has(obj2));

Caching + Memoization

  • Caching - storing the results of a heavy operation for later retrieval
  • Memoization - caching function calls by their parameters (works only for pure functions)

Avoid Garbage Collection

Avoid Unnecessary Immutability

  • I can hear the FPers groaning in the background!
  • The fact remains that fast code is often the opposite of clean code. If you need to be fast, `Object.assign({}, obj)` is a deoptimization
  • Especially avoid dogma, even if it's from ESLint

Don't do things you don't need

  • Fastest code is no code! O(0) beats O(1)
  • This is super generic advice, but here's some specifics. Are you...

Other JS Magic Learnings

  • Surprise... I won't teach them to you!
  • They're useful, but they're arbitrary and ever-changing
  • Best to file bugs with JS Engines than succumb to wisdom of elders
  • Instead, here's a list of things that were magic (but fixed)
    • try/catch couldn't be optimized
    • function character length (including comments!) dictated whether the function could be inlined
    • regex literals weren't auto-inlined
    • This IIFE `(function a(){}())` triggered eager parse but this one `!function a(){}()` didn't

Perf Mechanics

  • Isolate Environment
  • Instrument Application
  • Form Hypothesis
  • Test Cheaply
  • Fix Issue
  • Verify Fix
  • Publish Results

Isolate Environment

  • Ensure you own your CPU, and that nothing is hogging it
    • Plug in your power cord

    • Close all applications except one tab of chrome
    • `docker-machine down`

    • `sudo mdutil -a -i off`

  • ​Use `nvm` to manage runtimes

  • Test in the most common envs for your users

Instrument Application

  • `node --inspect-brk file.js`
  • Open Chrome
  • Paste URL

Form Hypothesis

  • Use profiler to discover
    • hot spots
    • cold spots
    • GC pauses
  • Examine code to see what's happening
  • Identify possible source of slowness

Test Cheaply

  • Can you just comment out the code in question?
    • Great for "don't do what you don't need" fixes
  • Can I replace the return value with a dummy value?
    • Great for Caching/Memoization fixes
  • Can I quickly replace this with a Set or Map?
    • Great for Big O Notation fixes

Fix Issue

  • Ironically, the actual coding is often the simplest part.
  • Most perf fixes are boring.

Verify Fix

  • Ensure you run the code uninstrumented many times before and after the fix.
  • Record times for before and after.
  • Instrument again. Ensure that the time savings come from where you wanted them to come from! Oftentimes, one perf fix has unintended perf consequences elsewhere!

Publish Results

  • It isn't enough to say it's faster! As with most programming tasks, there is a social component!
  • Educate the community! Publish the findings and your process!
  • File bugs with browsers and compilers when something seems wonky. It works!

What's Next in JS Perf

  • Node 8.3.0
  • Web Assembly
  • Binary AST proposal
  • cipm - shameless plug

Node 8.3.0

WASM

  • Literally, Web Assembly. Intended to allow C and C++ to compile to the Web Browser in a more efficient bytecode than JS currently allowed.
  • Can call into and out of JS, has access to DOM, etc. everything that JS has.
  • Intended to be a language compile target, not an authoring language with native performance. Exciting times indeed.

Binary AST format

  • For JS to run in the browser, it must be downloaded, parsed, and executed, in that order.
  • Binary AST is:
    • Binary instead of text, which means a smaller payload size.
    • An AST, which means it's already parsed.
  • Currently in Stage 1 of TC39, and many folks doubt it'll get to Stage 4, but an interesting concept!

CIPM

  • npm has many use cases:
    • install from package-lock.json OR package.json OR node_modules dirs
    • publish, update, link
  • cipm cuts the fat and several parts of npm's workflow, and is intended only for "install as part of a CI workflow":
    • installs from package-lock.json
    • run lifecycle scripts for the installed packages
    • should result in ~20 - 30% speed boost 
  • I can use the help, if anyone wants to get involved!

Gentle Reminder:

Anyone can do this!

Comments?

Questions?