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)
- https://github.com/npm/read-package-json/pull/70
- https://github.com/npm/hosted-git-info/pull/24
- Careful when caching objects... if they are mutated somewhere else, it mutates the cache! oops!
- Ensure your function is pure... params like `filename` often indicate "read from disk", which is impure! oops!
- Careful in general! Caching is indistinguishable from a memory leak and can cause GC
Avoid Garbage Collection
- Js is reference counted. After a certain amount of allocation, JS pauses and does GC to free up memory.
- Less Allocation == Less GC.
- Use profiler to find GC events, and see if a common function is preceding them!
- Hoist nested function declarations. Typically in the form of `forEach(() => x)`.
- Use WeakMaps to avoid dangling references!
- Great for memoization!
-
Gotcha, regex literals are allocations! Hoist them!
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
- Replaces Crankshaft with Turbofan + Ignition
- YMMV, but expected 10-20% across the board perf gains
- Much easier to implement new optimizations going forward
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?
Javascript Performance
By mikesherov
Javascript Performance
- 5,846