由於 Browser 只有一個 UI Thread 特性,若同時在此 Thread 執行很耗 CPU 運算,將使得 UI 有 Freeze 感覺,若能改用 Web Worker 執行耗 CPU 部分,將大幅提升使用者體驗。
Version
Vue 3.0.11
Comlink 4.3.0
Web Worker
輸入為 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 也要聆聽 message
event 接收 data。
在 callback 內就可執行假設 很耗時
的 toUpperCase()
,最後再以 postMessage()
將結果傳回 Vue。
可發現 Web Worker 基本上就是使用
postMessage()
與addEventListener('message')
彼此傳遞 data,雖然能達成在另一個 thread 執行耗 CPU 運算,但其 event-based API 實在很難用
Comlink
$ 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
結果不變,但使用 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
statetoUpper()
:將值轉成大寫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