-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
54bb7ff
commit f2c3077
Showing
7 changed files
with
201 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ( | ||
<div> | ||
Seconds: {this.state.seconds} | ||
</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 = ( | ||
<section className="main"> | ||
<input | ||
id="toggle-all" | ||
className="toggle-all" | ||
type="checkbox" | ||
onChange={this.toggleAll} | ||
checked={activeTodoCount === 0} | ||
/> | ||
<label | ||
htmlFor="toggle-all" | ||
/> | ||
<ul className="todo-list"> | ||
{todoItems} | ||
</ul> | ||
</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><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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters