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

JavaScript Async for Effortless UX

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Jaeseok Kang Jaeseok Kang
September 03, 2019

JavaScript Async for Effortless UX

Page freeze, glitchy animation, and slow scrolling. They follow us everywhere, and we'd like them to stop. This talk will get to the bottom of these issues by delving into the Javascript engine and concepts like task, call-stack, and event-loops. I will also introduce several ways to tackle these problems and walk you through a demo to help you understand.

Avatar for Jaeseok Kang

Jaeseok Kang

September 03, 2019
Tweet

More Decks by Jaeseok Kang

Other Decks in Programming

Transcript

  1. Cause Solution • "Page freeze.." • "Responding to user input

    too slow.." • "Glitchy animations.." • ... Problem • run-to-completion • javascript engine • call stack • event loop • task queue • ... • worker • scheduling • ...
  2. User Experience Users' feelings about using a product, system or

    service https://userexperiencerocks.wordpress.com/2015/08/20/then-my-kiddo-asked-whats-the-difference-between-ux-ui/
  3. Run-to-completion • Each message is processed completely before any other

    message is processed. let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  4. let running = true setTimeout(() => { console.log('Will it be

    print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  5. 1.set running to true let running = true setTimeout(() =>

    { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  6. 1.set running to true 2.run setTimeout function let running =

    true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  7. 1.set running to true 2.run setTimeout function 3.start while loop

    block let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  8. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  9. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed 5.print log 'Running...' let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  10. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed 5.print log 'Running...' 6.500ms later... running still true let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  11. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed 5.print log 'Running...' 6.500ms later... running still true 7.running is true as ever.. let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  12. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed 5.print log 'Running...' 6.500ms later... running still true 7.running is true as ever.. 8.and ever... let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  13. 1.set running to true 2.run setTimeout function 3.start while loop

    block 4.check if running have changed 5.print log 'Running...' 6.500ms later... running still true 7.running is true as ever.. 8.and ever... 9. ...forever.... let running = true setTimeout(() => { console.log('Will it be print?') }, 500) while(true) { if (!running) { break } console.log('Running...', Date.now()) }
  14. function hello() { console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea')

    } helloJsConf() helloJsConf() hello() main() console.log("hello")
  15. function someExpensive() { ... } function hello() { someExpensive() console.log('hello')

    } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf()
  16. main() function someExpensive() { ... } function hello() { someExpensive()

    console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf()
  17. helloJsConf() main() function someExpensive() { ... } function hello() {

    someExpensive() console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf()
  18. helloJsConf() hello() main() function someExpensive() { ... } function hello()

    { someExpensive() console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf()
  19. helloJsConf() hello() main() function someExpensive() { ... } function hello()

    { someExpensive() console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf() someExpensive()
  20. helloJsConf() hello() main() function someExpensive() { ... } function hello()

    { someExpensive() console.log('hello') } function helloJsConf() { hello() console.log('JSConfKorea') } helloJsConf() someExpensive() '
  21. Event loop 1. While there are tasks: • execute the

    oldest task. 2. Sleep until a task appears, then go to 1.
  22. Task & Microtask •Task Task that should execute sequentially in

    browser Source : setTimeout, running scripts, UI events.. •Microtask Async task that should happen after the currently executing script Microtask queue has a higher priority than the task queue. Source : Promise, MutationObserver, process.nextTick
  23. Event loop 1. While there are tasks: • execute the

    oldest task. 2. Sleep until a task appears, then go to 1.
  24. Event loop - Detail 1. If the microtask queue is

    not empty, execute all microtasks 2. While there are tasks: • execute the oldest task. 3. Sleep until a task appears, then go to 1.
  25. Call Stack Tasks Event Loop hello Task jsConfKorea Micro Task

    Microtasks setTimeout(() => { console.log('hello') }) Promise.resolve().then(() => { console.log('JSConfKorea') })
  26. Call Stack Tasks Event Loop hello Task jsConfKorea Micro Task

    Microtasks setTimeout(() => { console.log('hello') }) Promise.resolve().then(() => { console.log('JSConfKorea') })
  27. Call Stack Tasks Event Loop hello Task Microtasks setTimeout(() =>

    { console.log('hello') }) Promise.resolve().then(() => { console.log('JSConfKorea') })
  28. Call Stack Tasks Event Loop hello Task Microtasks setTimeout(() =>

    { console.log('hello') }) Promise.resolve().then(() => { console.log('JSConfKorea') })
  29. Call Stack Tasks Event Loop Microtasks setTimeout(() => { console.log('hello')

    }) Promise.resolve().then(() => { console.log('JSConfKorea') })
  30. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Call Stack Event Loop Tasks Microtasks
  31. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) setTimeout() Call Stack Event Loop Tasks Microtasks
  32. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Call Stack Event Loop hello JSConfKorea Task Tasks Microtasks
  33. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Promise.resolve.then() Call Stack Event Loop hello JSConfKorea Task Tasks Microtasks
  34. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Call Stack Event Loop hello JSConfKorea Task someExpensive Micro Task Tasks Microtasks
  35. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Call Stack Event Loop hello JSConfKorea Task someExpensive Micro Task Tasks Microtasks
  36. function someExpensive() { ... } setTimeout(() => { console.log('hello JSConfKorea')

    }) Promise.resolve().then(() => { someExpensive() }) Call Stack Event Loop hello JSConfKorea Task someExpensive Micro Task ' Tasks Microtasks
  37. • Task is always executed sequentially by event loop. Other

    task cannot be performed when any task is running. • Microtask queue has a higher priority than the task queue. So, UI-related events cannot be executed again until all microtasks accumulated in the queue are cleared. • What if long running stacked tasks or microtasks block event that is directly connected to UI such as rendering, click, input? janky UI/UX occurs... ,
  38. inputEl.addEventListener('input', (e) => { const textLength = e.target.value.length let result

    = '' for (let i; i < getSquareCounts(textLength); i++) { result += makeSquareWithRandomColorHtml() } containerEl.innerHTML = result })
  39. inputEl.addEventListener('input', (e) => { const textLength = e.target.value.length let result

    = '' for (let i; i < getSquareCounts(textLength); i++) { result += makeSquareWithRandomColorHtml() } containerEl.innerHTML = result }) Too many iterations
  40. inputEl.addEventListener('input', (e) => { const textLength = e.target.value.length let result

    = '' for (let i; i < getSquareCounts(textLength); i++) { result += makeSquareWithRandomColorHtml() } containerEl.innerHTML = result }) Too many iterations Costly DOM changes
  41. Web Workers •Web Workers makes it possible to run a

    script operation in a background thread separate from the main execution thread of a web application. https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
  42. Main Thread Worker Thread 2. result = runLongRunningTask() 1. postMessage()

    3. postMessage(result) 4. doSomethingWith(result)
  43. self.addEventListener('message', e => { const result = runLongRunningTask() postMessage(result) })

    // spawn a worker const worker = new Worker('worker.js') // send messages to a worker for request run long-runnging-task worker.postMessage(message) // handle a `message` event from a worker worker.onmessage = e => { doSomethingWith(e) } worker.js main.js
  44. Limitations • Data is sent between workers and the main

    thread via a system of messages. • Worker cannot access directly the DOM, context.
  45. function* chunkGenerator (textLength) { let result = '' for (let

    i = 0; i < getSquareCounts(textLength) / CHUNK_UNIT; i++) { for (let j = 0; j < CHUNK_UNIT; j++) { result = makeSquareWithRandomColorHtml() } yield } containerEl.innerHTML = result } inputEl.addEventListener('input', (e) => { const textLength = e.target.value.length runChunks(chunkGenerator(textLength)) }) inputEl.addEventListener('input', (e) => { const textLength = e.target.value.length let result = '' for (let i = 0; i < getSquareCounts(textLength); i++) { result += makeSquareWithRandomColorHtml() } containerEl.innerHTML = result }) AS-IS TO-BE https://github.com/jaeseokk/chunk-scheduler
  46. Recap • If long-running-tasks or microtasks block rendering, click, and

    text input, a janky UI that harms user experience can be delivered can occur. • This is due to the structure of the JavaScript engine, event- loop, etc. and needs to understand and handle properly. • To handle this, Delegate long-running-tasks to other threads using Web Worker, • Or, split the long-running-task properly so that other important UI events do not block.