import { CodeSurfer, CodeSurferColumns, Step } from "code-surfer" import { nightOwl, duotoneDark, shadesOfPurple } from "@code-surfer/themes"
import { themes } from 'mdx-deck' export const theme = { ...themes.yellow, styles: { h1: { margin: "10px 0" }, h2: { margin: "5px 0" }, h3: { margin: "5px 0" }, p: { margin: "5px 0" }, li: { margin: "10px 0" } } }
<small style={{ position: "absolute", right: "15px", bottom: "15px" }}> Source: https://github.com/pbadenski/building-high-performance
Twitter: @pbadenski
Email: [email protected]
Mentor @ Meet-a-Mentor
<small style={{ position: "absolute", right: "15px", bottom: "15px" }}> *) shameful plug - we're looking for talented programmers!
nearly 15 years of experience in building software
coming from Java background, became "JS developer" 5 years ago
generalist, but loves performance engineering
... this presentation included 😉
majority of performance suggestion online don't apply to our project
<Image src={require("./static/prom-client-closed-pr.png")} size="contain" height="40%" />
we're a specialised SaaS platform for financial analytics
our main JS bundle is 6MB and 1.5MB compressed
we don't care about download/parse time of JS code
<Image src={require("./static/chrome-performance-report.png")} size="contain" height="40%" />
...even though 99% of the Internet does
our performance challenges are managing
thousands of financial computations inside the browser
<Image src={require("./static/grafana-calculation-rate.png")} size="contain" height="30%" width="90%" />
high performance means a different thing for every project
for us it means
optimised for a CPU-bounded problem with high event throughput
- 500-1000 Redux actions per second
- 10^2 computations per second
- each at 10-50ms == between 0.1 & 5 seconds for a single "computation loop"
Hint: We use Web Workers
<Image src={require("./static/npm-web-workers.png")} size="contain" height="50%" width="80%" />
BTW CPU-bounded is a fancy talk for:
"if you get more CPUs your program will go faster
WHILE increasing other resources will deliver exactly 0 value"
4 years ago we were eagerly waiting for async/await to show up in Chrome
<Image src={require("./static/chrome-async-await-status.png")} size="contain" height="40%" />
we haven't worried about Chrome/V8 updates for over 2 years now
These days JS is pretty darn fast
...but JIT optimised languages usually come with caveats
<Image src={require("./static/v8-caveats.png")} size="contain" height="40%" width="80%" />
...and even more caveats
<Image src={require("./static/v8-caveats-2.png")} size="contain" height="40%" width="80%" />
Some big unsolved problems...
- crazy number of string representations
- difficult to understand performance characteristics
- no builtin hashCode, no interning
object polymorphism / megamorphism
<Image src={require("./static/megamorphic.png")} size="contain" height="40%" width="80%" />
Recommended: deoptigate
slow Date implementation
<Image src={require("./static/v8-group-date-thread.png")} size="contain" height="40%" />
WebAssembly isn't likely to be an alternative yet, but getting there...
For more details: Scala.js and WebAssembly, a tale of the dangers of the sea
Our journey with managing asynchronous behaviour:
-> async calls embedded in React components
| -> thunks
| -> Redux Saga
| -> RxJS
| -> https://www.npmjs.com/package/rxjs-redux
<CodeSurferColumns themes={[nightOwl, shadesOfPurple]}>
const onKeyPressed = async (props: Props) => {
const result = await callApi();
props.dispatch(makeAction(result));
};
const InteractiveComponent = (props: Props) => (
<input onKeyPress={() => onKeyPressed(props)}></input>
);
<CodeSurferColumns themes={[nightOwl, shadesOfPurple]}>
// thunk.ts
const actionThunk = async (
dispatch: Dispatch, getState: () => State
) => {
const result = await callApi();
dispatch(makeAction(result));
};
// component.tsx
const onKeyPressed = (props: Props) => {
props.dispatch(actionThunk);
};
const InteractiveComponent = ...
<CodeSurferColumns themes={[nightOwl, shadesOfPurple]}>
// saga.ts
function* request(action: RequestAction) {
const result = yield call(callApi);
yield put(makeAction(result));
}
function* mySaga() {
yield takeEvery("REQUEST_ACTION", request);
}
// component.tsx
const onKeyPressed = (props: Props) => {
props.dispatch({ type: "REQUEST_ACTION" });
};
const InteractiveComponent = ...
<CodeSurferColumns themes={[nightOwl, shadesOfPurple]}>
// epic.ts
action$
.pipe(
ofType("REQUEST_ACTION"),
flatMap(async action => {
const result = await callApi();
return makeAction(result);
})
);
// component.tsx
const onKeyPressed = (props: Props) => {
props.dispatch({ type: "REQUEST_ACTION" });
};
const InteractiveComponent = ...
Exercise left to the reader 😉
Keep using boring technology
unless it gives you competitive advantage
Competitive advantage - solves problem at the core of your domain
A lot of people pay high cost of RxJS complexity
<Image src={require("./static/rxjs-complexity.png")} size="contain" height="40%" width="80%" />
Often the cost might not be worth the benefits
(based on conversations @ https://gitter.im/Reactive-Extensions/RxJS)
We chose React & Redux on the 1st day
In retrospect we could have chosen more lightweight view library
...but we didn't know any better
...and we're quite happy with our choice otherwise
And I think it's ok to choose a "popular" solution
if you don't know any better :D
We tweaked our usage of Redux with:
- ImmutableJS - Immutable persistent data collections for Javascript
- reselect
- https://www.npmjs.com/package/redux-log-slow-reducers
- https://www.npmjs.com/package/redux-ignore
We learnt that ironically React isn't necessarily reactive...
no builtin debouncing of rendering
we use https://npmjs.com/package/react-debounce-render
no built-in debouncing of prop-updates
... and impossible to my best knowledge
we use lazy data structures (eg. ImmutableJS.Seq) as a work around
Other reactive alternatives we might try one day
Easy to read and well modularised code is significantly easier to optimise
It's like cooking a three course dinner in a messy kitchen - sure possible, but...
Simpler code is often faster for a computer to execute
Evolving great design is a topic of thousands of presentations,
so very quickly about our experiences...
We started our project in TypeScript in 2016
(when everyone laughed at us for using this exotic language)
Recommended: https://effectivetypescript.com
Monads FTW!
<Image src={require("./static/monads.png")} size="contain" height="40%" width="80%" />
Unit test your architecture https://www.npmjs.com/package/dependency-cruiser
<Image src={require("./static/unit-test-architecture.png")} size="contain" height="40%" width="80%" />
Inspired by (https://www.archunit.org/ via ThoughtWorks Tech Radar)
Domain Driven Design still alive
<Image src={require("./static/project-structure.png")} size="contain" height="40%" />
Also check out Ducks (https://github.com/erikras/ducks-modular-redux)
YMMV tools require a bit of getting used to - try playing violin 😆
ImmutableJS has been a blessing
...even though it's been pretty much abandoned for the last 2 years
<Image src={require("./static/immutablejs-unmaintained.png")} size="contain" height="40%" />
Records suck
<Image src={require("./static/immutablejs-records.png")} size="contain" height="40%" />
if starting a new project consider other options:
for a while we used IndexedDB as a cache
it eventually grew to millions of entries
and schema upgrade put user's CPU into a death spiral
we've been using combineReducers wrong for nearly 2 years
suffering a 20%-30% performance penalty
another time we upgraded to HTTP 2.0 only to realise
that many companies downgrade requests in their network stack
solving complex and challenging problems is a journey
taking chances is inevitable if you want to learn
- Don’t get distracted by other people problems
- You can build a high performance web app with JS
- Choose boring technology... and then don’t!
- Pick your battles
- Great performance starts with great design.
- Immutability is awesome, but comes at a cost.
- "Mistakes were made"
Questions? Comments? Observations?