由於 Browser 只有一個 UI Thread 特性,若同時在此 Thread 執行很耗 CPU 運算,將使得 UI 有 Freeze 感覺,若能改用 Web Worker 執行耗 CPU 部分,將大幅提升使用者體驗。
Version
Vue 2.6.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 Worker Loader
$ yarn add worker-loader --dev
由於 Vue CLI 是基於 Webpack,因此安裝 worker-loader
讓 Web Worker 能在 Webpack 下使用。
App.vue
<template>
<div>
<input v-model="message">
<button @click="onClick">Uppercase</button>
{{ result }}
</div>
</template>
<script>
import WebWorker from 'worker-loader!./webWorker'
let webWorker = new WebWorker
let onClick = function() {
webWorker.postMessage(this.message)
webWorker.addEventListener('message', x => this.result = x.data)
}
export default {
data: () => ({
message: '',
result: ''
}),
methods: {
onClick
}
}
</script>
10 行
import WebWorker from 'worker-loader!./webWorker'
引入 webWorker.js
,稍後會將 Web Worker thread 的 code 寫在此。
12 行
let webWorker = new WebWorker
建立 Web Worker。
14 行
let onClick = function() {
webWorker.postMessage(this.message)
webWorker.addEventListener('message', x => this.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>
<div>
<input v-model="message">
<button @click="onClick">Uppercase</button>
{{ result }}
</div>
</template>
<script>
import WebWorker from 'worker-loader!./webWorker'
import { wrap } from 'comlink'
let { toUpper } = wrap(new WebWorker)
let onClick = function() {
toUpper(this.message).then(
x => this.result = x
)
}
export default {
data: () => ({
message: '',
result: ''
}),
methods: {
onClick
}
}
</script>
11 行
import { wrap } from 'comlink'
從 comlink 引入 wrap()
。
13 行
let { toUpper } = wrap(new WebWorker)
使用 wrap()
將 Web Worker Object 包成 comlink 自己的 object,並且 destructure 出自行提供的 toUpper()
。
15 行
let onClick = function() {
toUpper(this.message).then(
x => this.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。
Function Pipeline
App.vue
<template>
<div>
<input v-model="message">
<button @click="onClick">Uppercase</button>
{{ result }}
</div>
</template>
<script>
import { pipe, andThen as then, always } from 'ramda'
import WebWorker from 'worker-loader!./webWorker'
import { wrap } from 'comlink'
let { toUpper } = wrap(new WebWorker)
let onClick = function() {
pipe(
always(this.message),
toUpper,
then(x => this.result = x)
)()
}
export default {
data: () => ({
message: '',
result: ''
}),
methods: {
onClick
}
}
</script>
16 行
let onClick = function() {
pipe(
always(this.message),
toUpper,
then(x => this.result = x)
)()
}
由於 toUpper()
回傳為 Promise,因此可輕易整合在 Function Pipeline 中。
webWorker.js
import { expose } from 'comlink'
import { toUpper } from 'ramda'
expose({ toUpper })
toUpper()
也可改用 Ramda 版本。
Conclusion
- 本範例僅用簡單的
toUpper()
示意,實務上可以改成自定義很耗 CPU 的 function - Comlink 明顯比 Web Worker 原生 API 好用,且由於是回傳 Promise 可輕易 Function Pipeline
Reference
Игорь Харченко, Vue Performance Optimization Using a Web Worker
MDN, Web Worker API
Webpack, worker-loader
GoogleChromeLabs, comlink