Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Secrets of the DOM Virtual Machine

tomdale
December 01, 2017

Secrets of the DOM Virtual Machine

Most web applications today build their UIs by running JavaScript. But what happens if we instead treat our templates as a specialized functional programming language that can be compiled into optimized bytecode and executed with a virtual machine? In this talk, we'll explore this novel architecture and some of the performance benefits it gives us.

tomdale

December 01, 2017
Tweet

More Decks by tomdale

Other Decks in Programming

Transcript

  1. Lightweight ๏ Downloads fast. ๏ Parses fast. ๏ Renders fast.

    Responsive ๏ Tap or click → updates fast. ๏ Sophisticated animations. ๏ 60fps scrolling.
  2. Lightweight ๏ Downloads fast. ๏ Parses fast. ๏ Renders fast.

    Responsive ๏ Tap or click → updates fast. ๏ Sophisticated animations. ๏ 60fps scrolling. TENSION
  3. Virtual DOM ๏ Compiles JSX to JavaScript ๏ Renders recursively,

    synchronously ๏ Pushes optimization responsibility to application code
  4. function Weather(weather) { return [ <h2>Current Weather</h2>, <address> New York,

    NY </address>, <div> <strong> 78° </strong> </div> ]; } function Weather(weather) { return [React.createElement( "h2", null, "Current Weather" ), React.createElement( "address", null, "New York, NY" ), React.createElement( "div", null, React.createElement( "strong", null, "78\xB0" ) )]; }
  5. COMPILER <h2>Current Weather</h2> <address> New York, NY </address> <div> <strong>

    {{weather.temperature}}° </strong> </div> <button is="repl-button" onclick={{action @onClick}}> {{@title}} </button>
  6. 0x19 0x00 0x1f 0x16 0x01 0x20 0x16 0x02 0x19 0x03

    0x1f 0x16 0x04 0x20 0x16 0x02 0x19 0x05 0x1f 0x16 0x06 0x19 0x07 0x1f 0x16 0x08 0x30 0x15 0x48 0x04 0x00 0x05 0x09 0x05 0x0a 0x0d 0x03 0x00 0x39 0x32 0x02 0x2f 0x36 0x3c 0x00 0x13 0x04 0x0d 0x03 0x01 0x0f 0x04 0x30 0x0b 0x13 0x28 0x0b 0x13 0x0b 0x13 0x28 0x0b 0x13 0x0b 0x13 0x28 0x0b 0x13 0x3d 0x00 0x07 0x3f 0x3f 0x04 0x47 0x25 0x40 0x00 0x04 0x41 0x04 0x4a 0x04 0x45 0x04 0x46 0x4a 0x04 0x31 0x13 0x26 0x48 0x0f 0x04 0x33 0x14 0x18 0x00 0x33 0x14 0x31 0x16 0x0b 0x20 0x16 0x0c 0x20 0x14 <h2>Current Weather</h2> <address> New York, NY </address> <div> <strong> {{weather.temperature}}° </strong> </div>
  7. 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x0b Primitive primitive:

    0x13 19 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x0b Primitive primitive: 0x13 19 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x3d PushArgs names: 0x00 0 positionals: 0x07 7 synthetic: 0x3f 319 0x3f PrepareArgs state: 0x04 4 0x47 BeginComponentTransaction 0x25 PushDynamicScope 0x40 CreateComponent flags: 0x00 0 state: 0x04 4 0x41 RegisterComponentDestructor state: 0x04 4 0x43 GetComponentSelf state: 0x04 4 0x45 GetComponentLayout state: 0x04 4 0x46 InvokeComponentLayout 0x4a DidRenderLayout state: 0x04 4 0x31 PopFrame 0x13 PopScope 0x26 PopDynamicScope 0x48 CommitComponentTransaction 0x0f Load register: 0x04 4 0x33 Exit 0x14 Return 0x18 DynamicContent trusting: 0x00 0 0x33 Exit 0x14 Return 0x31 PopFrame 0x16 Text text: 0x0b "°\n " 0x20 CloseElement 0x16 Text text: 0x0c "\n" 0x20 CloseElement 0x14 Return 0x19 OpenElement tag: 0x00 "h2" 0x1f FlushElement 0x16 Text text: 0x01 "Current Weather" 0x20 CloseElement 0x16 Text text: 0x02 "\n\n" 0x19 OpenElement tag: 0x03 "address" 0x1f FlushElement 0x16 Text text: 0x04 "\n New York, NY\n" 0x20 CloseElement 0x16 Text text: 0x02 "\n\n" 0x19 OpenElement tag: 0x05 "div" 0x1f FlushElement 0x16 Text text: 0x06 "\n " 0x19 OpenElement tag: 0x07 "strong" 0x1f FlushElement 0x16 Text text: 0x08 "\n " 0x30 PushFrame 0x15 ReturnTo offset: 0x48 72 0x04 GetVariable symbol: 0x00 0 0x05 GetProperty key: 0x09 "weather" 0x05 GetProperty key: 0x0a "temperature" 0x0d Dup register: 0x03 3 offset: 0x00 0 0x39 IsComponent 0x32 Enter args: 0x02 2 0x2f JumpUnless to: 0x36 54 0x3c PushDynamicComponentManager meta: 0x00 0 0x10 Fetch register: 0x04 4 0x0d Dup register: 0x03 3 offset: 0x01 1 0x0f Load register: 0x04 4 0x30 PushFrame 0x0b Primitive primitive: 0x13 19
  8. 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x0b Primitive primitive:

    0x13 19 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x0b Primitive primitive: 0x13 19 0x28 PushBlockScope 0x0b Primitive primitive: 0x13 19 0x3d PushArgs names: 0x00 0 positionals: 0x07 7 synthetic: 0x3f 319 0x3f PrepareArgs state: 0x04 4 0x47 BeginComponentTransaction 0x25 PushDynamicScope 0x40 CreateComponent flags: 0x00 0 state: 0x04 4 0x41 RegisterComponentDestructor state: 0x04 4 0x43 GetComponentSelf state: 0x04 4 0x45 GetComponentLayout state: 0x04 4 0x46 InvokeComponentLayout 0x4a DidRenderLayout state: 0x04 4 0x31 PopFrame 0x13 PopScope 0x26 PopDynamicScope 0x48 CommitComponentTransaction 0x0f Load register: 0x04 4 0x33 Exit 0x14 Return 0x18 DynamicContent trusting: 0x00 0 0x33 Exit 0x14 Return 0x31 PopFrame 0x16 Text text: 0x0b "°\n " 0x20 CloseElement 0x16 Text text: 0x0c "\n" 0x20 CloseElement 0x14 Return 0x19 OpenElement tag: 0x00 "h2" 0x1f FlushElement 0x16 Text text: 0x01 "Current Weather" 0x20 CloseElement 0x16 Text text: 0x02 "\n\n" 0x19 OpenElement tag: 0x03 "address" 0x1f FlushElement 0x16 Text text: 0x04 "\n New York, NY\n" 0x20 CloseElement 0x16 Text text: 0x02 "\n\n" 0x19 OpenElement tag: 0x05 "div" 0x1f FlushElement 0x16 Text text: 0x06 "\n " 0x19 OpenElement
  9. <h2>Current Weather</h2> <address> New York, NY </address> <div> <strong> 78°

    </strong> </div> {strings:["h2","Current Weather","\n\n","address","\n New York, NY\n","div","\n ","strong","\n 78°\n ","\n"]} 19 01 00 00 1F 00 16 01 01 00 20 00 16 01 02 00 19 01 03 00 1F 00 16 01 04 00 20 00 16 01 02 00 19 01 05 00 1F 00 16 01 06 00 19 01 07 00 1F 00 16 01 08 00 20 00 16 01 09 00 20 00 14 00
  10. export class Heap { private heap: Uint16Array | Array<number>; private

    table: number[]; private offset = 0; private handle = 0; constructor(serializedHeap?: { buffer: ArrayBuffer, table: number[], handle: number }) { if (serializedHeap) { let { buffer, table, handle } = serializedHeap; this.heap = new Uint16Array(buffer); this.table = table; this.offset = this.heap.length; this.handle = handle; } else { this.heap = new Uint16Array(0x100000); this.table = []; } } push(item: number): void { this.heap[this.offset++] = item; } getbyaddr(address: number): number { return this.heap[address]; Glimmer VM uses ArrayBuffer directly, no parse or intermediate data structure.
  11. ๏ Tiny runtime ๏ Compact bytecode ๏ No parse is

    faster than no parse 1 Instant Templates
  12. Lightweight ๏ Downloads fast. ๏ Parses fast. ๏ Renders fast.

    Responsive ๏ Tap or click → updates fast. ๏ Sophisticated animations. ๏ 60fps scrolling.
  13. “What makes a React app slow is, most of the

    time, useless rerenders of many components. You may have read that the React VirtualDom is super fast. That's true, but in a medium size app, a full redraw can easily render hundreds of components. Even the fastest VirtualDom templating engine can't make that in less than 16ms.”
  14. “The React documentation is very clear about the way to

    avoid useless rerenders: shouldComponentUpdate(). … it's your job as a developer to check that the props of a component didn't change and skip rendering altogether in that case.”
  15. 0x0019 OpenElement tag: 0x0000 "div" 0x001c StaticAttr name: 0x0001 "class"

    value: 0x0002 "active" namespace: 0x0000 0 0x001f FlushElement 0x0004 GetVariable symbol: 0x0000 0 0x0005 GetProperty key: 0x0003 "firstName" 0x0018 DynamicContent trusting: 0x0000 0 0x0020 CloseElement 0x0014 Return <div class="active"> {{firstName}} </div>
  16. import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import

    Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends React.Component { componentDidMount() { let counter = 0; setInterval(() => { this.setState({ counter: ++counter }); }, 0); } render() { let { state } = this; let { counter } = state || {}; return <div> <h1>{ counter }</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends Component { @tracked counter = 0; didInsertElement() { setInterval(() => { this.counter++; }, 0); } static template = ` <h1>{{counter}}</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> <BlackSpurs /> <Chorizomena /> <VyacheslavShishkov /> <JafarShafaghat /> REACT GLIMMER
  17. import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import

    Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends React.Component { componentDidMount() { let counter = 0; setInterval(() => { this.setState({ counter: ++counter }); }, 0); } render() { let { state } = this; let { counter } = state || {}; return <div> <h1>{ counter }</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends Component { @tracked counter = 0; didInsertElement() { setInterval(() => { this.counter++; }, 0); } static template = ` <h1>{{counter}}</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> <BlackSpurs /> <Chorizomena /> <VyacheslavShishkov /> <JafarShafaghat /> REACT GLIMMER
  18. import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import

    Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends React.Component { componentDidMount() { let counter = 0; setInterval(() => { this.setState({ counter: ++counter }); }, 0); } render() { let { state } = this; let { counter } = state || {}; return <div> <h1>{ counter }</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends Component { @tracked counter = 0; didInsertElement() { setInterval(() => { this.counter++; }, 0); } static template = ` <h1>{{counter}}</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> <BlackSpurs /> <Chorizomena /> <VyacheslavShishkov /> <JafarShafaghat /> REACT GLIMMER
  19. import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import

    Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends React.Component { componentDidMount() { let counter = 0; setInterval(() => { this.setState({ counter: ++counter }); }, 0); } render() { let { state } = this; let { counter } = state || {}; return <div> <h1>{ counter }</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> import RudyRufer from './RudyRufer'; import TheSunHasGotHisHatOn from './ TheSunHasGotHisHatOn'; import Roenno from './Roenno'; import LisaSchulte from './LisaSchulte'; import ChasinWildTrains from './ChasinWildTrains'; import TommyWinship from './TommyWinship'; import Parepepeotes from './Parepepeotes'; export default class extends Component { @tracked counter = 0; didInsertElement() { setInterval(() => { this.counter++; }, 0); } static template = ` <h1>{{counter}}</h1> <PrebleNewYork /> <Section25oftheConstitutionofAustralia /> <Exabit /> <AllahabadLalehzar /> <Abrin /> <WeekendatBerniesalbum /> <GarbatkaMasovianVoivodeship /> <Taconystation /> <BlackSpurs /> <Chorizomena /> <VyacheslavShishkov /> <JafarShafaghat /> REACT GLIMMER
  20. 2 Optimized Updates • With Virtual DOM, sluggish update performance

    can sneak up on you • Generating optimized bytecode during initial render keeps updates fast
  21. render(iterator: TemplateIterator): void { // Iterate the template iterator, executing

    the compiled template program // until there are no more instructions left to execute. let result; do { result = iterator.next(); } while (!result.done); this.result = result.value; }
  22. render(iterator: TemplateIterator): Promise<void> { return new Promise((resolve) => { let

    timeout = this.timeout; let tick = (deadline: Deadline) => { let iteratorResult: IteratorResult<RenderResult>; do { iteratorResult = iterator.next(); } while (!iteratorResult.done && deadline.timeRemaining() > 1); if (iteratorResult.done) { this.result = iteratorResult.value; return resolve(); } requestIdleCallback(tick, { timeout }); }; requestIdleCallback(tick, { timeout }); }); }
  23. • Initial render can take multiple seconds on low-end phones

    • Raw optimization has reached diminishing returns—we need to break up the work. • VM architecture make this almost too easy. • Works with rehydration, too! 2 60fps Incremental Rendering