點燈坊

失くすものさえない今が強くなるチャンスよ

Web Worker 初體驗

Sam Xiao's Avatar 2021-04-15

由於 Browser 只有一個 UI Thread 特性,若同時在此 Thread 執行很耗 CPU 運算,將使得 UI 有 Freeze 感覺,若能改用 Web Worker 執行耗 CPU 部分,將大幅提升使用者體驗。

Version

Vue 3.0.11
Comlink 4.3.0

Web Worker

worker000

輸入為 Sam,按下 Uppercase button 後輸出全大寫的 SAM

假設 toUpper() 是一個很耗 CPU 的 synchronous function,這使得按下 button 後,整個畫面會 freeze。

由於 Browser 只有一個 UI thread,若將很耗 CPU 的邏輯也執行在 UI thread,勢必造成整個畫面 freeze,必較好的方式是將很耗 CPU 部分改寫在 Web Worker thread,如此將不會影響 UI thread 執行

Add Woker Loader

$ yarn add worker-loader --dev

由於 Vue CLI 是基於 Webpack,因此安裝 worker-loader 讓 Web Worker 能在 Webpack 下使用。

Component

App.vue

<template lang='pug'>
input(v-model='message')/
button(@click='onClick') Uppercase
spa {{ result }}
</template>

<script setup>
import WebWorker from 'worker-loader!./webWorker'

let webWorker = new WebWorker

ref: message = ''
ref: result = ''

let onClick = () => {
  webWorker.postMessage(message)
  webWorker.addEventListener('message', x => result = x.data)
}
</script>

第 9 行

import WebWorker from 'worker-loader!./webWorker'

引入 webWorker.js,稍後會將 Web Worker thread 的 code 寫在此。

11 行

let webWorker = new WebWorker

建立 Web Worker。

16 行

let onClick = () => {
  webWorker.postMessage(message)
  webWorker.addEventListener('message', x => result = x.data)
}
  • 使用 postMessage() 將 data 傳入 Web Worker
  • 使用 addEventListener('message') 聆聽 message event,Web Worker 將藉此 message event 從 Web Worker thread 傳出資料

由於 Web Worker 為 asynchronous,因此藉由 event 傳遞 data 可以理解,但 event 畢竟還是沒 Promise 好用,若能使用 Promise 就太好了

webWorker.js

self.addEventListener('message',
  x => self.postMessage(x.data.toUpperCase())
)

在 Web Worker thread 一律使用 self 存取 Web Worker。

由於 Vue 使用 postMessage() 對 Web Worker 傳送 data,因此 Web Worker 也要聆聽 messageevent 接收 data。

在 callback 內就可執行假設 很耗時toUpperCase(),最後再以 postMessage() 將結果傳回 Vue。

可發現 Web Worker 基本上就是使用 postMessage()addEventListener('message') 彼此傳遞 data,雖然能達成在另一個 thread 執行耗 CPU 運算,但其 event-based API 實在很難用

$ yarn add comlink

安裝 GoogleChromeLabs 的 comlink,讓我們以 promise-based API 使用 Web Worker。

App.vue

<template lang='pug'>
input(v-model='message')/
button(@click='onClick') Uppercase
span {{ result }}
</template>

<script setup>
import { ref } from 'vue'
import WebWorker from 'worker-loader!./webWorker'
import { wrap } from 'comlink'

let { toUpper } = wrap(new WebWorker)

ref: message = ''
ref: result = ''

let onClick = () => toUpper(message).then(x => result = x)
</script>

11 行

import { wrap } from 'comlink'

從 comlink 引入 wrap()

13 行

let { toUpper } = wrap(new WebWorker)

使用 wrap() 將 Web Worker Object 包成 comlink 自己的 object,並且 destructure 出自行提供的 toUpper()

18 行

let onClick = () => toUpper(message).then(x => result = x)

由於 comlink 會使 function 都從 event-based 改成 promise-based,因此 toUpper() 回傳 Promise,可在 then() 寫入 side effect 在 HTML template 顯示。

webWorker.js

import { expose } from 'comlink'

let toUpper = msg => msg.toUpperCase()

expose({ toUpper })

第 1 行

import { expose } from 'comlink'

從 comlink 引入 expose()

第 3 行

let toUpper = msg => msg.toUpperCase()

定義自己很耗 CPU 的 function。

第 5 行

expose({ toUpperCase })

使用 expose() 將自定義 funciton 包成 Object 傳入,expose() 將回傳 Web Worker。

Point-free

worker000

結果不變,但使用 Point-free 改寫。

App.vue

<template lang='pug'>
input(v-model='message')/
button(@click='onClick') Uppercase
span {{ result }}
</template>

<script setup>
import { pipe, andThen as then } from 'ramda'
import { ref } from 'vue'
import { read, write } from 'vue3-fp'
import WebWorker from 'worker-loader!./webWorker'
import { wrap } from 'comlink'

let { toUpper } = wrap(new WebWorker)

let message = ref('')
let result = ref('')

let onClick = pipe(
  read(message),
  x => toUpper(x),
  then(write(result))
)
</script>

19 行

let onClick = pipe(
  read(message),
  x => toUpper(x),
  then(write(result))
)

使用 pipe() 組合出 onClick()

  • read():讀取 message state
  • toUpper():將值轉成大寫
  • write():寫入 result state

這裏 x => toUpper(x) 無法使用 point-free 版本的 toUpper()

webWorker.js

import { expose } from 'comlink'
import { toUpper } from 'ramda'

expose({ toUpper })

toUpper() 也可改用 Ramda 版本。

Conclusion

  • 本範例僅用簡單的 toUpper() 示意,實務上可以改成自定義很耗 CPU 的 function
  • Comlink 明顯比 Web Worker 原生 API 好用,且由於是回傳 Promise 可輕易 Point-free

Reference

Игорь Харченко, Vue Performance Optimization Using a Web Worker
MDN, Web Worker API
Webpack, worker-loader
GoogleChromeLabs, comlink