Secrets of the DOM Virtual Machine

9bf3a766e037b9d5a4da0a6f9d0f4f68?s=47 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.

9bf3a766e037b9d5a4da0a6f9d0f4f68?s=128

tomdale

December 01, 2017
Tweet

Transcript

  1. None
  2. SLOW FAST

  3. D O W N LO A D RENDER PARSE

  4. Lightweight ๏ Downloads fast. ๏ Parses fast. ๏ Renders fast.

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

    Responsive ๏ Tap or click → updates fast. ๏ Sophisticated animations. ๏ 60fps scrolling. TENSION
  6. Virtual DOM

  7. VIRTUAL DOM

  8. Virtual DOM ๏ Compiles JSX to JavaScript ๏ Renders recursively,

    synchronously ๏ Pushes optimization responsibility to application code
  9. None
  10. None
  11. 1 3 2 Instant Templates 60fps Incremental Rendering Optimized Updates

  12. 1 Instant Templates

  13. https://medium.com/@samccone/performance-futures-bundling-281543d9a0d5 DOWNLOAD PARSE COMPILE EXECUTE 130MS 200MS

  14. https://medium.com/reloading/javascript-start-up-performance-69200f43b201

  15. 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" ) )]; }
  16. https://medium.com/reloading/javascript-start-up-performance-69200f43b201

  17. <h2>Current Weather</h2> <address> New York, NY </address> <div> <strong> {{weather.temperature}}°

    </strong> </div>
  18. COMPILER

  19. 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>
  20. COMPILER

  21. COMPILER

  22. COMPILER ?

  23. COMPILER 101010 010101 101010

  24. BYTECODE

  25. 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>
  26. 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
  27. 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
  28. COMPILER VM 101010 010101 101010 BUILD TIME BROWSER

  29. COMPILER VM 101010 010101 101010 BUILD TIME BROWSER

  30. <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
  31. https://medium.com/@samccone/performance-futures-bundling-281543d9a0d5 DOWNLOAD PARSE COMPILE EXECUTE 130MS 200MS

  32. None
  33. DOWNLOAD EXECUTE 130MS PARSE COMPILE 200MS

  34. DOWNLOAD EXECUTE 130MS

  35. DOWNLOAD EXECUTE 130MS

  36. 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.
  37. Demo

  38. None
  39. None
  40. ๏ Tiny runtime ๏ Compact bytecode ๏ No parse is

    faster than no parse 1 Instant Templates
  41. 1 3 2 Instant Templates 60fps Incremental Rendering Optimized Updates

  42. 2 Optimized Updates

  43. Lightweight ๏ Downloads fast. ๏ Parses fast. ๏ Renders fast.

    Responsive ๏ Tap or click → updates fast. ๏ Sophisticated animations. ๏ 60fps scrolling.
  44. None
  45. “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.”
  46. “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.”
  47. GLIMMER VM

  48. APPENDING VM UPDATING VM

  49. <div class="active"> {{firstName}} </div>

  50. <div class="active"> {{firstName}} </div> STATIC DYNAMIC

  51. 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>
  52. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM DOM

  53. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM DOM

  54. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div DOM

  55. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div DOM

  56. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div class="active">

    DOM
  57. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div class="active">

    DOM
  58. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div class="active">

    DOM
  59. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div class="active">

    DOM
  60. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM <div class="active">

    Sean DOM
  61. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    <div class="active"> Sean DOM
  62. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> Sean DOM
  63. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> Sean DOM
  64. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> Sean </div> DOM
  65. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> Sean </div> DOM
  66. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> Sean </div> DOM
  67. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent <div class="active"> </div> DOM Brendan
  68. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent
  69. StaticAttr OpenElement GetVariable GetProperty DynamicContent CloseElement APPEND PROGRAM UPDATE PROGRAM

    UpdateDynamicContent 6× LESS WORK
  70. Demo

  71. 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
  72. 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
  73. 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
  74. 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
  75. None
  76. None
  77. 2 Optimized Updates • With Virtual DOM, sluggish update performance

    can sneak up on you • Generating optimized bytecode during initial render keeps updates fast
  78. 1 3 2 Instant Templates 60fps Incremental Rendering Optimized Updates

  79. 3 60fps Incremental Rendering

  80. e Frame Frame Frame Frame F

  81. s 16ms 16ms 16ms 16ms 1

  82. s 16ms 16ms 16ms 16ms 1 1000ms ÷ 60 =

    16.6 ̅ms
  83. s 70ms 1

  84. s 70ms 1

  85. 1000ms+

  86. 1000ms+ WHITE RECTANGLE OF DOOM

  87. OpenElement FlushElement Text CloseElement GetVariable GetProperty DynamicContent SYNCHRONOUS

  88. OpenElement FlushElement requestIdleCallback 16ms Text CloseElement GetVariable requestIdleCallback 16ms GetProperty

    DynamicContent requestIdleCallback 16ms
  89. OpenElement FlushElement requestIdleCallback 16ms Text CloseElement GetVariable requestIdleCallback 16ms GetProperty

    DynamicContent requestIdleCallback 16ms
  90. Demo

  91. None
  92. None
  93. 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; }
  94. 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 }); }); }
  95. None
  96. None
  97. • 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
  98. 1 3 2 Instant Templates 60fps Incremental Rendering Optimized Updates

  99. Future ๏ Implement VM in Web Assembly ๏ Execute programs

    in Web Worker
  100. https://try.glimmerjs.com

  101. https://try.glimmerjs.com

  102. None
  103. Thank you! @tomdale