React中使用多線程—Web Worke
前言
作為一個(gè)前端開發(fā),如果你還停留在每天CRUD,還停留在切圖/畫圖,還停留在和后端同學(xué)對某個(gè)API設(shè)計(jì)的是否合理而大打出手時(shí),是時(shí)候停下來了。我們要變強(qiáng),我們需要對我們經(jīng)手的項(xiàng)目進(jìn)行一番改造和優(yōu)化。這才是我們能夠變強(qiáng)的方式。而不是,沉浸在無休止的爭吵和埋怨中。
眾所周知,Javascript是一種「單線程語言」。因此,如果我們執(zhí)行任何耗時(shí)任務(wù),它將阻塞UI交互。用戶需要等待任務(wù)完成才能執(zhí)行其他操作,這會(huì)給用戶體驗(yàn)帶來不好的影響。
其實(shí),針對此類問題,我們有很多解決方案,
- 例如將耗時(shí)任務(wù)分割成多個(gè)短任務(wù),并讓其在多個(gè)渲染幀內(nèi)執(zhí)行,給UI交互(也就是UI渲染)留有時(shí)間,
- 也可以通過回調(diào)的方式,在UI交互觸發(fā)后,在進(jìn)行耗時(shí)任務(wù)的操作。
- 亦或者我們可以指定一個(gè)「優(yōu)先隊(duì)列」,當(dāng)高優(yōu)先級任務(wù)被執(zhí)行時(shí),低優(yōu)先級任務(wù)(耗時(shí)任務(wù))被降級處理(冷處理),直到高優(yōu)先級任務(wù)被執(zhí)行后再執(zhí)行剩余低優(yōu)先級任務(wù)。(這其實(shí)就是React并發(fā)的核心要點(diǎn))
- ...等等
上述列舉了很多解決方式,他們都有一個(gè)共同特點(diǎn) - 由于JS單線程屬性,它們只是將一些耗時(shí)任務(wù)從一個(gè)渲染幀分割或者延后到多個(gè)渲染幀內(nèi)。本質(zhì)上還是單線程的處理方式。
而,今天我們就介紹一種利用「多線程(Web Worker)處理React中的耗時(shí)操作」。我們之前也在前面講過Web Worker的相關(guān)內(nèi)容。
- Web性能優(yōu)化之Worker線程(上)
- Web性能優(yōu)化之Worker線程(下)
今天我們就詳細(xì)的介紹如何在前端項(xiàng)目中使用Web Worker用于處理耗時(shí)任務(wù),然后將長任務(wù)利用多線程的分割出主線程,然后給主線程留足時(shí)間去回應(yīng)更緊急的用戶操作,優(yōu)化用戶操作。
好了,天不早了,干點(diǎn)正事哇。
我們能所學(xué)到的知識點(diǎn)
- Web Workers
- React 的并發(fā)模式
React 中使用Web Worker
- useWorker
- Web Worker的注意點(diǎn)
1. Web Workers
雖然,在之前的文章中介紹過Web Worker,但是為了最大限度的兼容大家的學(xué)習(xí)情況,還是打算簡單介紹一些。
圖片
如上圖所示,JS中存在三中Worker,按照實(shí)現(xiàn)可以分為三類。
- Web Worker
- Shared Web Worker
- Service Worker
而我們今天的主角-Web Worker是我們最常見的。
Web Worker是在后臺運(yùn)行的腳本,不會(huì)影響用戶界面,因?yàn)樗凇竼为?dú)的線程中運(yùn)行」,而不是在主線程中。
因此,它不會(huì)導(dǎo)致任何阻塞用戶交互。Web Worker主要用于在Web瀏覽器中執(zhí)行耗時(shí)任務(wù),如對大量數(shù)據(jù)進(jìn)行排序、CSV導(dǎo)出、圖像處理等。
圖片
從上圖中,如果耗時(shí)任務(wù)在主線程中執(zhí)行會(huì)阻塞UI渲染,當(dāng)用Web Worker代理耗時(shí)任務(wù)后,主線程并不會(huì)發(fā)生阻塞,也就是說「它強(qiáng)任它強(qiáng),老子Web Worker」
2. React 的并發(fā)模式
講到這里,可能有些心細(xì)的小伙伴就會(huì)產(chǎn)生疑問。既然都是處理耗時(shí)任務(wù)。那么,React 18的并發(fā)渲染也可以達(dá)到此種目的。也就是使用React.useTransition()將耗時(shí)任務(wù)設(shè)定為過渡任務(wù),通過對某些操作標(biāo)記為「低優(yōu)先級」,在頁面渲染過程中給「高優(yōu)先級」的任務(wù)讓步。
之前我們在
- React 18 如何提升應(yīng)用性能
- React 并發(fā)原理
中,對React 并發(fā)有過介紹。(想了解更多可以翻閱上述文章)。這里我們就簡單闡述一下為什么React 并發(fā)只是錦上添花,缺不能藥到病除。
如果,你仔細(xì)看過上面的文章,你就會(huì)有有一個(gè)清晰的認(rèn)知:
React并發(fā)模式并不會(huì)并行運(yùn)行任務(wù)。它會(huì)將非緊急任務(wù)移動(dòng)到過渡狀態(tài),并立即執(zhí)行緊急任務(wù)。它「使用相同的主線程」來處理它。
下面是之前的一個(gè)示例。
圖片
使用useTransition只是告知React,有一些操作是不緊急的,如果遇到更高級的任務(wù),不緊急的任務(wù)可以不立馬顯示,而是在處理完高優(yōu)先級任務(wù)后才進(jìn)行低優(yōu)先級任務(wù)的渲染。
圖片
例如,如果一個(gè)表格正在渲染一個(gè)大型數(shù)據(jù)集,而用戶嘗試搜索某些內(nèi)容,React會(huì)將任務(wù)切換到用戶搜索并首先處理它。
圖片
正如我們在圖片中看到的那樣,
「緊急任務(wù)是通過上下文切換」來處理的
React的并發(fā)模式,只是讓我們的項(xiàng)目「擁有了辨別優(yōu)先級的能力」,并且在「一定限制條件下」能夠快速響應(yīng)用戶操作。但是,但是,但是,如果一個(gè)「單個(gè)任務(wù)已經(jīng)超過了瀏覽器一幀的渲染時(shí)間」,那雖然設(shè)置了startTransition,但是也「無能為力」。如果存在這種情況,那就只能人為的將單個(gè)任務(wù)繼續(xù)拆分或者利用Web Worker進(jìn)行多線程處理了。
當(dāng)使用Web Worker進(jìn)行相同任務(wù)時(shí),表格渲染會(huì)在一個(gè)獨(dú)立的線程中并行運(yùn)行。
圖片
3. React 中使用Web Worker
由于我們在項(xiàng)目開發(fā)時(shí),使用不同的打包工具(vite/webpack)。幸運(yùn)的是,最新版的vite/webpack都支持Web Worker了。
我們可以通過
- new URL()的方式 --vite/webpack都支持
new Worker(
new URL(
'./worker.js',
import.meta.url
)
);
- import方式 只有vite支持
import MyWorker from './worker?worker'
const worker = new MyWorker()
更詳細(xì)的處理可以參考它們的官網(wǎng)
- vite_web_worker[1]
- webpack_web_worker[2]
當(dāng)然,我們在項(xiàng)目代碼中如何實(shí)例化Worker對象也有很多方式。下面就介紹兩種。
通過引入文件路徑
index.js
// 創(chuàng)建一個(gè)新的Worker對象,
// 指定要在Worker線程中執(zhí)行的腳本文件路徑
const myWorker = new Worker(
new URL('./worker.js', import.meta.url)
);
// 向Worker發(fā)送消息
myWorker.postMessage(789789);
// 監(jiān)聽來自Worker的消息
myWorker.onmessage = function(event) {
console.log("來自worker的消息: ", event.data);
};
worker.js
// 在Worker腳本中接收并處理消息
self.onmessage = function(event) {
console.log("來自主線程的消息: ", event.data);
// 執(zhí)行一些計(jì)算密集型的任務(wù)
let result = doSomeHeavyTask(event.data);
// 將結(jié)果發(fā)送回主線程
self.postMessage(result);
};
const doSomeHeavyTask = (num) => {
// 模擬一些計(jì)算密集型的操作
let result = 0;
for (let i = 0; i < num; i++) {
result += i;
}
return result;
};
Blob 方式
index.js
// 定義要在Worker中執(zhí)行的腳本內(nèi)容
const workerScript = `
self.onmessage = function(e) {
console.log('來自主線程的消息: ' + e.data);
self.postMessage('向主線程發(fā)送消息: ' + 'Hello, ' + e.data);
};
`;
// 創(chuàng)建一個(gè)Blob對象,指定腳本內(nèi)容和類型
const blob = new Blob(
[workerScript],
{ type: 'application/javascript' }
);
// 使用URL.createObjectURL()方法創(chuàng)建一個(gè)URL,用于生成Worker
const blobURL = URL.createObjectURL(blob);
// 生成一個(gè)新的Worker
const worker = new Worker(blobURL);
// 監(jiān)聽來自Worker的消息
worker.onmessage = function(e) {
console.log('來自worker的消息: ' + e.data);
};
// 向Worker發(fā)送消息
worker.postMessage('Front789');
使用Blob構(gòu)建方式生成Web Worker有以下幾個(gè)優(yōu)勢:
優(yōu)勢 | 描述 |
動(dòng)態(tài)生成 | 可以動(dòng)態(tài)地生成 |
內(nèi)聯(lián)腳本 | 將 |
便捷性 | 更方便地創(chuàng)建和管理 |
安全性 |
|
總的來說,使用Blob構(gòu)建方式生成Web Worker可以提供更靈活、便捷和安全的方式來管理和使用Worker實(shí)例。
4. useWorker
上面一節(jié)中,我們介紹了如何在前端項(xiàng)目中使用Web Worker。無論是使用文件導(dǎo)入的方式還是Blob的方式。都需要寫一些模板代碼。雖然能解決我們的問題,但是使用方式還是不夠優(yōu)雅。
功能介紹
下面,我們就介紹一種更優(yōu)雅的方式- 使用useWorker庫。
useWorker[3]是一個(gè)庫,它使用React Hooks在簡單的配置中使用Web Worker API。它支持在不阻塞UI的情況下執(zhí)行耗時(shí)任務(wù),支持使用Promise而不是事件監(jiān)聽器。
我們可以從官網(wǎng)看到相關(guān)的介紹信息。
圖片
其中,WORKER_STATUS用于返回Web Worker的狀態(tài)信息。
圖片
我們可以通過向useWorker中傳遞一個(gè)回調(diào)函數(shù),然后該函數(shù)就會(huì)在對應(yīng)的Web Worker中執(zhí)行。
const sortNumbers = numbers => ([...numbers].sort())
const [
sortWorker,
{
status: sortStatus,
kill: killSortWorker
}
] = useWorker(sortNumbers);
大家可以對比之前的用原生構(gòu)建Web Worker實(shí)例。我們可以拋棄冗余代碼,并且返回的函數(shù)(sortWorker)還支持Promise。
也就意味著我們使用xx.then()或者 await xx()以同步的寫法獲取異步結(jié)果。
import React from "react";
import { useWorker } from "@koale/useworker";
const numbers = [...Array(5000000)].map(
e => ~~(Math.random() * 1000000)
);
const sortNumbers = nums => nums.sort();
const Example = () => {
const [sortWorker] = useWorker(sortNumbers);
const runSort = async () => {
const result = await sortWorker(numbers);
};
return (
<button type="button" onClick={runSort}>
運(yùn)行耗時(shí)任務(wù)
</button>
);
};
并且,useWorker是一個(gè)大小為3KB的庫,我們還不需要有太多的資源負(fù)擔(dān)。既然,有這么多強(qiáng)勢的功能,那我們就來看看它到底是何方神圣。
安裝依賴
用我們御用腳手架f_cli[4],來構(gòu)建一個(gè)前端項(xiàng)目(npx f_cli_f craete worker_demo)。
要將useWorker()添加到React項(xiàng)目中,請使用以下命令:
npm install @koale/useworker --force
由于useworker源碼中使用了peerDependencies指定了React版本為^16.8.0。如果大家在17/18版本的React環(huán)境下,會(huì)發(fā)生錯(cuò)誤。所以我們可以使用--force忽略版本限制。(這里大家可以放心使用,它內(nèi)部的只是用到簡單的hook)
安裝完包后,導(dǎo)入useWorker()。
import {
useWorker,
WORKER_STATUS
} from "@koale/useworker";
我們從庫中導(dǎo)入useWorker和WORKER_STATUS。useWorker()鉤子返回workerFn和controller。
- workerFn是一個(gè)允許在Web Worker中運(yùn)行函數(shù)的函數(shù)。
- controller包含status和kill參數(shù)。
status參數(shù)返回Worker的狀態(tài)
kill函數(shù)用于終止當(dāng)前運(yùn)行的Worker
案例展示
讓我們通過一個(gè)示例來看看useWorker()。
使用useWorker()和主線程對大數(shù)組進(jìn)行排序
SortingArray
首先,創(chuàng)建一個(gè)SortingArray組件,并添加以下代碼:
工具代碼
// 模擬耗時(shí)任務(wù)
const bubleSort = (arr: number[]): number[] =>{
const len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
const numbers = [...Array(50000)].map(() =>
Math.floor(Math.random() * 1000000)
);
主要邏輯
import React,{ useState } from "react";
import {
useWorker,
WORKER_STATUS
} from "@koale/useworker";
function SortingArray() {
const [sortStatus, setSortStatus] = useState(false);
const [
sortWorker,
{
status: sortWorkerStatus
}
] = useWorker(bubleSort);
console.log("WebWorker status:", sortWorkerStatus);
const onSortClick = () => {
setSortStatus(true);
const result = bubleSort(numbers);
setSortStatus(false);
alert('耗時(shí)任務(wù)結(jié)束!')
console.log("處理結(jié)果", result);
};
const onWorkerSortClick = () => {
sortWorker(numbers).then((result) => {
console.log("使用WebWorker的處理結(jié)果", result);
alert('耗時(shí)任務(wù)結(jié)束!')
});
};
return (
<div>
<section >
<button
type="button"
disabled={sortStatus}
onClick={() => onSortClick()}
>
{sortStatus ?
`正在處理耗時(shí)任務(wù)...` :
`主線程觸發(fā)耗時(shí)任務(wù)`
}
</button>
<button
type="button"
disabled={sortWorkerStatus === WORKER_STATUS.RUNNING}
onClick={() => onWorkerSortClick()}
>
{sortWorkerStatus === WORKER_STATUS.RUNNING
? `正在處理耗時(shí)任務(wù)...`
: `使用WebWorker處理耗時(shí)任務(wù)`
}
</button>
</section>
<section>
<span style={{ color: "white" }}>
打開控制臺查驗(yàn)狀態(tài)信息
</span>
</section>
</div>
);
}
export default SortingArray;
我們在SortingArray配置了兩個(gè)操作
- onSortClick中按照常規(guī)處理,也就是在主線程中執(zhí)行耗時(shí)操作
- onWorkerSortClick 中執(zhí)行useWorker相關(guān)邏輯,并傳遞了bubleSort函數(shù)以使用Worker執(zhí)行耗時(shí)的排序操作。
App.js
我們App.js中引入SortingArray組件,并且為了能讓UI阻塞看的更明顯,我們用JS來操作logo文件,讓其不停的轉(zhuǎn)動(dòng),每100毫秒旋轉(zhuǎn)一次。
- 如果是一個(gè)阻塞主線程的任務(wù),那么logo將會(huì)停止
- 如果主線程不阻塞,那logo會(huì)一直轉(zhuǎn)動(dòng)
import React from "react";
import SortingArray from "./SortingArray";
import logo from './assets/react.svg'
import "./App.css";
let turn = 0;
function infiniteLoop() {
const lgoo = document.querySelector(".logo");
turn += 8;
lgoo.style.transform = `rotate(${turn % 360}deg)`;
}
export default function App() {
React.useEffect(() => {
const loopInterval = setInterval(infiniteLoop, 100);
return () => clearInterval(loopInterval);
}, []);
return (
<>
<div >
<h1 >useWorker Demo</h1>
<header>
<img src={logo} className="logo" />
</header>
<hr />
</div>
<div>
<SortingArray />
</div>
</>
);
}
我們來看看分別點(diǎn)擊對應(yīng)按鈕會(huì)發(fā)生啥?
上圖是耗時(shí)任務(wù)在主線程中執(zhí)行的效果。在執(zhí)行期間,動(dòng)畫效果是阻塞的,也就意味著在多個(gè)幀的時(shí)間內(nèi),瀏覽器是無法執(zhí)行額外的操作的。
我們用Chrome-performance來探查一下性能消耗。
我們可以看到事件:點(diǎn)擊任務(wù)花費(fèi)了7.85秒來完成整個(gè)過程,并且它阻塞了主線程7.85秒。
圖片
而這個(gè)圖,我們使用了Web Worker,在執(zhí)行耗時(shí)任務(wù)的時(shí)候,動(dòng)畫還是執(zhí)行原來的操作。也就是操作不會(huì)阻塞。因?yàn)閡seWorker在后臺執(zhí)行排序而不阻塞UI。這使得用戶體驗(yàn)非常流暢。
和上面的分析方式一樣,打開Performancetab,讓我們看看這種方法的性能分析結(jié)果。
圖片
我們截取主線程的部分?jǐn)?shù)據(jù),發(fā)現(xiàn)有任意時(shí)間段內(nèi),Scripting所占總時(shí)間的比例都很少,更大部分都是Idle也就是主線程處于空閑階段,可以隨時(shí)響應(yīng)用戶操作。
圖片
而在對應(yīng)的worker中確是一直在執(zhí)行計(jì)算任務(wù),絲毫沒有片刻休息。
5. Web Worker的注意點(diǎn)
何時(shí)用Worker
我們之前的文章講過,JS自從引入V8[5]后,在代碼執(zhí)行和內(nèi)存處理上有了更高的優(yōu)化。例如使用JIT[6],引入WebAssembly[7],熱代碼優(yōu)先編譯等。
但是呢,針對一些特殊的場景,上述的方式只能提供簡單的優(yōu)化,這樣我們就需要另外的解決方案來處理這些棘手的問題。
當(dāng)我們遇到如下情景,并有嚴(yán)重的性能問題,那就需要借助Web Worker一臂之力了
- 圖像處理
- 對大型數(shù)據(jù)集進(jìn)行排序或處理
- 帶有大量數(shù)據(jù)的CSV或Excel導(dǎo)出
- 畫布繪制
- 任何CPU密集型任務(wù)
Worker的限制
這個(gè)在之前介紹Web Worker的文章就介紹過,我們就直接拿來主義了。
- Web Worker無法訪問window對象和document。
- 當(dāng)Worker正在運(yùn)行時(shí),我們無法再次調(diào)用它,直到它完成或被終止。為了解決這個(gè)問題,我們可以創(chuàng)建兩個(gè)或更多useWorker()鉤子的實(shí)例。
- Web Worker無法返回函數(shù),因?yàn)轫憫?yīng)是序列化的。
- Web Worker受到終端用戶機(jī)器可用CPU核心和內(nèi)存的限制。
Reference
[1]vite_web_worker:https://cn.vitejs.dev/guide/features.html#web-workers
[2]webpack_web_worker:https://webpack.js.org/guides/web-workers/
[3]useWorker:https://github.com/alewin/useWorker
[4]f_cli:https://www.npmjs.com/package/f_cli_f
[5]V8:https://v8.dev/
[6]JIT:https://v8.dev/blog/maglev
[7]WebAssembly:https://webassembly.github.io/spec/core/