Skip to content

Commit

Permalink
add mithril benchmark
Browse files Browse the repository at this point in the history
  • Loading branch information
leontrolski authored and oli-russell committed Apr 6, 2020
1 parent 54bb7ff commit f2c3077
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 42 deletions.
139 changes: 139 additions & 0 deletions 33-line-react-thoughts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<title>leontrolski - 33 line React</title>
<style>
body {margin: 5% auto; background: #fff7f7; color: #444444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.8; text-shadow: 0 1px 0 #ffffff; max-width: 63%;}
@media screen and (max-width: 800px) {body {font-size: 14px; line-height: 1.4; max-width: 90%;}}
code {background: #f0f0f0;}
pre {width: 100%; border-top: 3px solid gray; border-bottom: 3px solid gray;}
a {border-bottom: 1px solid #444444; color: #444444; text-decoration: none;}
a:hover {border-bottom: 0;}
</style>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/styles/default.css"></style>
<script src="https://unpkg.com/[email protected]/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</head>
<body>
<h1>33 line React - thoughts</h1>

Thanks

<p>
<em><a href="33-line-react.html">Original post</a>, <a href="https://news.ycombinator.com/item?id=22776753">original discussion</a>.</em>
</p>

<h2>TLDR</h2>
<p>Have a go building your next frontend with <em>an as simple as possible</em> <code>pageState -> DOM</code> model, maybe use <a href="https://mithril.js.org/">mithril</a>.</p>

<h2>Performance</h2>
<p>
I had a very unscientific play around with this neat <a href="https://localvoid.github.io/uibench/">benchmarking tool</a>, you can use the <code>Custom URL</code> "http://leontrolski.github.io/benchmark" to compare yourself. I'm going to keep the performance figures intentionally vague - I was comparing Vanilla JS, React 16 and 33-line.
<ul>
<li>React tended to be faster than 33-line by a factor of 2 to 10.</li>
<li>The Vanilla JS solution would often outperform 33-line.</li>
<li>For smaller DOM trees, often the <em>winner</em> was Vanilla JS (small discussion below).</li>
<li>Occasionally (particularly on the <code>tree</code> tests for some reason), 33-line would be fastest, but only by a tad.</li>
<li>The <a href="https://localvoid.github.io/uibench-react/16/main.js">React code</a> was (gzipped) 40kb of minified ugliness, the <a href="https://leontrolski.github.io/benchmark/main.js">33-line code</a> was (gzipped) 1.8kb of bog standard JS.</li>
<li>The "JS Init Time" of react would be 2 to 10 times slower than 33-line js, think in the order of 100ms.</li>
<li>The performance of 33-line got proportially worse as the number of divs increased, this makes sense, given the diff algorithm is <em>basic</em>.</li>
</ul>
</p>
<h3>Performance - Notes</h3>
<p>
The <a href="https://github.com/leontrolski/leontrolski.github.io/blob/4f9cea8a5afc55252d38eb1aa1a20eda264a880f/benchmark/main.js">Vanilla JS one</a> just shoves strings of html together and does a big <code>container.innerHTML =</code>, nice and simple. On the other hand, string munging sucks for obvious reasons. Also, you end up with a lot of updates flashing around in the devtools inspector - this alone might kill the approach for a lot of people.
</p>
<p>
I had to write a <a href="https://github.com/leontrolski/leontrolski.github.io/blob/54bb7ff011065f0d46ae8f2e3c841dc3aa30c157/benchmark/main.js#L67-L69">few extra lines</a> to handle <code>data-</code> attributes, that cranked it up to 37 lines. I think if you were to try productionise this toy code you'd end up with about 3/4 of a <a href="https://mithril.js.org/">mithril</a>.
</p>
<p>
I did one run with the <a href="https://github.com/Freak613/stage0">stage0</a> library thingy, the code was a bit more <a href="https://github.com/Freak613/stage0/blob/master/examples/uibench/app.js">gnarly</a>, but it was <em>rapid</em>. If I was writing eg. a game or a big spreadsheet app that needed high performance, I'd definitely be considering a library in this space.
</p>
<p>
Things that I'd imagine React particularly excels at versus a naive approach would be things like clock counters/animations - small bits of the DOM changing at high frequency - the tradeoff for this performance seems to be API complexity. If one is to use a simpler <code>pageState -> DOM</code> model with few library hooks into the guts, it may be necessary to implement clocks etc. out of band of the normal library's render loop.
</p>

<h2><code>.jsx</code>, state management and aesthetics</h2>
<p>
The React homepage, has the following snippet:
</p>
<pre><code>class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
tick() {
this.setState(state => ({
seconds: state.seconds + 1
}));
}
componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return (
&lt;div>
Seconds: {this.state.seconds}
&lt;/div>
);
}
}</code></pre>
<p>
Versus, for example, the code for the noughts and crosses in my <a href="33-line-react.html">original post</a>, there's a huge amount of ceremony. I have to:
<ul>
<li>Do some OO gubbins.</li>
<li>Wrap all calls to update the state with <code>this.setState(...)</code>.</li>
<li>Conform with quite a large API.</li>
<li>Constantly pass state around with <code>props</code> and <code>this.state</code> (I understand some of this has been sorted with hooks, right?).</li>
<li>Compile the JSX to boring js.</li>
</ul>
There are alleged performance/codebase management reasons for some of these, but I remain a bit sceptical of their applicability.
</p>
<h3>Ezz-thetic</h3>
<p>
To my eyes, the original mithril <a href="https://raw.githack.com/MithrilJS/mithril.js/master/examples/todomvc/todomvc.js">TodoMVC source</a> is exceptionally expressive and handsome, especially versus the equivalent React <a href="https://github.com/tastejs/todomvc/tree/gh-pages/examples/react">example</a>. Maybe I'm turning into Arthur Whitney, but I'm kind of enjoying long, dense lines like:
</p>
<pre><code>m("section#main", {style: {display: state.todos.length > 0 ? "" : "none"}}, [
m("input#toggle-all[type='checkbox']", {checked: state.remaining === 0, onclick: ui.toggleAll})
...</code></pre>
<p>Consider the recommended React/JSX equivalent:</p>
<pre><code>if (todos.length) {
main = (
&lt;section className="main">
&lt;input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
&lt;label
htmlFor="toggle-all"
/>
&lt;ul className="todo-list">
{todoItems}
&lt;/ul>
&lt;/section>
);
}</code></pre>
<h3>Routing</h3>
<p>
As a consumer of webpages, I'm not sure I've ever seen the URL change in an SPA and thought "phewph, I'm glad I wasn't redirected to a new page" - just gimme a normal <code>&lt;a></code> yo.
</p>

<h2>Hacker news meta bit</h2>
<p>
For a while, the top-voted thread was people moaning about how a variable was called <code>m</code>, then a later comment in the code said it was a <code>grid</code>. I agree it was maybe a bit annoying, but I dunno, you read the article and that was your takeaway.. I've been part of a fair few code reviews with this vibe in my time :-)
</p>

<h2>Conclusions</h2>
<p>
Doing <code>document.querySelectorAll('*')</code> on the airbnb map view (a reasonably complex SPA) returns 3407 elements. With no thought to performance at all, a simple library can render in the order of 100s of divs per <em>millisecond</em>. You could probably swap React out with 33-line on most sites and no-one would notice, you could also swap it out with some Vanilla JS string munging too - although the developer egonomics would be a bit rubbish.
</p>
<p>
If your site's slow (unless you're something really complicated like a game/spreadsheet), it's probably that you put a lot of crap on it, rather than anything to do with how you render your divs.
</p>
</body>
3 changes: 3 additions & 0 deletions 33-line-react.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
</head>
<body>
<h1>33 line React</h1>
<p>
<em><a href="33-line-react-thoughts.html">Thoughts</a> on reading through the hacker news <a href="https://news.ycombinator.com/item?id=22776753">response</a>.</em>
</p>
<p>
<a href="https://reactjs.org/">React</a>
<ul>
Expand Down
37 changes: 37 additions & 0 deletions benchmark/37-line-react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const m = (...args)=>{
let [attrs, [head, ...tail]] = [{}, args]
let [tag, ...classes] = head.split('.')
if (tail.length && !m.isRenderable(tail[0])) [attrs, ...tail] = tail
if (attrs.class) classes = [...classes, ...attrs.class]
attrs = {...attrs}; delete attrs.class
const children = []
const addChildren = v=>v === null? null : Array.isArray(v)? v.map(addChildren) : children.push(v)
addChildren(tail)
return {__m: true, tag: tag || 'div', attrs, classes, children}
}
m.isRenderable = v =>v === null || ['string', 'number'].includes(typeof v) || v.__m || Array.isArray(v)
m.update = (el, v)=>{
if (!v.__m) return el.data === `${v}` || (el.data = v)
for (const name of v.classes) if (!el.classList.contains(name)) el.classList.add(name)
for (const name of el.classList) if (!v.classes.includes(name)) el.classList.remove(name)
const normal = Object.keys(v.attrs).filter(name=>!name.startsWith('data-'))
for (const name of normal) if (el[name] !== v.attrs[name]) el[name] = v.attrs[name]
for (const {name} of el.attributes) if (!normal.includes(name) && name !== 'class' && !name.startsWith('data-')) el.removeAttribute(name)
const data = Object.keys(v.attrs).filter(name=>name.startsWith('data-')).map(name=>name.slice(5))
for (const name of data) if (el.dataset[name] !== `${v.attrs[`data-${name}`]}`) el.dataset[name] = v.attrs[`data-${name}`]
for (const name of Object.keys(el.dataset)) if (!data.includes(name)) delete el.dataset[name]
}
m.makeEl = v=>v.__m? document.createElement(v.tag) : document.createTextNode(v)
m.render = (parent, v)=>{
const olds = parent.childNodes || []
const news = v.children || []
for (const _ of Array(Math.max(0, olds.length - news.length))) parent.removeChild(parent.lastChild)
for (const [i, child] of news.entries()){
let el = olds[i] || m.makeEl(child)
if (!olds[i]) parent.appendChild(el)
const mismatch = (el.tagName || '') !== (child.tag || '').toUpperCase()
if (mismatch) (el = m.makeEl(child)) && parent.replaceChild(el, olds[i])
m.update(el, child)
m.render(el, child)
}
}
2 changes: 2 additions & 0 deletions benchmark/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<body>
<div id="App"></div>
<script src="https://localvoid.github.io/uibench-base/0.1.0/uibench.js"></script>
<script src="37-line-react.js"></script>
<script src="main.js"></script>
<script>uibench.init("33-line", "0.0.1")</script>
</body>
</html>
45 changes: 4 additions & 41 deletions benchmark/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ const TreeNode = data=>m('ul.TreeNode', data.children.map(n=>n.container? TreeNo
const Tree = data=>m('.Tree', TreeNode(data.tree.root))

// test
uibench.init("33-line", "0.0.1")
// uibench.init in the html (one per framework)
function renderState(container, state){
const location = state && state.location
const fs = {
table: Table,
anim: Anim,
tree: Tree,
}
m.render(container, {children: [m('.Main', fs[location](state))]})
// first for mithril, then 33-line
if (m.redraw) m.render(container, m('.Main', fs[location](state)))
else m.render(container, {children: [m('.Main', fs[location](state))]})
}
function renderSamples(samples){
document.body.innerHTML = "<pre>" + JSON.stringify(samples, null, " ") + "</pre>"
Expand All @@ -43,42 +45,3 @@ document.addEventListener("DOMContentLoaded", (e) => {
container = document.querySelector("#App")
uibench.run(state=>renderState(container, state), renderSamples)
})

// Framework:
const m = (...args)=>{
let [attrs, [head, ...tail]] = [{}, args]
let [tag, ...classes] = head.split('.')
if (tail.length && !m.isRenderable(tail[0])) [attrs, ...tail] = tail
if (attrs.class) classes = [...classes, ...attrs.class]
attrs = {...attrs}; delete attrs.class
const children = []
const addChildren = v=>v === null? null : Array.isArray(v)? v.map(addChildren) : children.push(v)
addChildren(tail)
return {__m: true, tag: tag || 'div', attrs, classes, children}
}
m.isRenderable = v =>v === null || ['string', 'number'].includes(typeof v) || v.__m || Array.isArray(v)
m.update = (el, v)=>{
if (!v.__m) return el.data === `${v}` || (el.data = v)
for (const name of v.classes) if (!el.classList.contains(name)) el.classList.add(name)
for (const name of el.classList) if (!v.classes.includes(name)) el.classList.remove(name)
const normal = Object.keys(v.attrs).filter(name=>!name.startsWith('data-'))
for (const name of normal) if (el[name] !== v.attrs[name]) el[name] = v.attrs[name]
for (const {name} of el.attributes) if (!normal.includes(name) && name !== 'class' && !name.startsWith('data-')) el.removeAttribute(name)
const data = Object.keys(v.attrs).filter(name=>name.startsWith('data-')).map(name=>name.slice(5))
for (const name of data) if (el.dataset[name] !== `${v.attrs[`data-${name}`]}`) el.dataset[name] = v.attrs[`data-${name}`]
for (const name of Object.keys(el.dataset)) if (!data.includes(name)) delete el.dataset[name]
}
m.makeEl = v=>v.__m? document.createElement(v.tag) : document.createTextNode(v)
m.render = (parent, v)=>{
const olds = parent.childNodes || []
const news = v.children || []
for (const _ of Array(Math.max(0, olds.length - news.length))) parent.removeChild(parent.lastChild)
for (const [i, child] of news.entries()){
let el = olds[i] || m.makeEl(child)
if (!olds[i]) parent.appendChild(el)
const mismatch = (el.tagName || '') !== (child.tag || '').toUpperCase()
if (mismatch) (el = m.makeEl(child)) && parent.replaceChild(el, olds[i])
m.update(el, child)
m.render(el, child)
}
}
15 changes: 15 additions & 0 deletions benchmark/mithril.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://localvoid.github.io/uibench-base/0.1.0/styles.css" rel="stylesheet" />
<title>UI Benchmark: Vanilla [innerHTML]</title>
</head>
<body>
<div id="App"></div>
<script src="https://localvoid.github.io/uibench-base/0.1.0/uibench.js"></script>
<script src="https://www.unpkg.com/[email protected]/mithril.min.js"></script>
<script src="main.js"></script>
<script>uibench.init("mithril", "2.0.4")</script>
</body>
</html>
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h1>Hi</h1>
<p>
Here are a series of posts where I reimplement a minimal version of something to demonstrate how it works.
<ul>
<li>33 line <a href="33-line-react.html">React</a>-like library</li>
<li>33 line <a href="33-line-react.html">React</a>-like library, and <a href="33-line-react-thoughts.html">followup</a></li>
<li>Ever wondered what the hell is going on with your <code>pandas.DataFrame</code>, check <a href="fake-data-frame.html">this</a> out</li>
<li>Reach OO/closure <a href="poor-mans-object.html">nirvana</a></li>
</ul>
Expand Down

0 comments on commit f2c3077

Please sign in to comment.