了解如何通过
Web Workers
和WebAssembly
显著提升JavaScript
应用的性能,并使用斐波那契算法作为案例分析。
JavaScript
通常运行在单线程上,通常称为“主线程”。这意味着 JavaScript
以同步方式一次执行一个任务。主线程还负责渲染任务,例如页面绘制和布局,以及处理用户交互,这也意味着长时间运行的 JavaScript
任务会导致浏览器变得无响应。这就是为什么当一个耗时的 JavaScript
函数运行时,网页可能会“卡住”,阻碍用户的正常操作。
我们将通过模拟斐波那契算法的繁重计算来演示如何阻塞主线程,并将通过以下几种方法来解决主线程阻塞的问题:
- 多线程(
Web Worker
) - 使用
AssemblyScript
的WebAssembly
- 使用
Rust
的WebAssembly
斐波那契算法
在本文的所有案例研究中,我们将使用一个简单且非常常见的斐波那契算法,其时间复杂度为 O(2^n)
。
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
单线程
现在,让我们直接在主线程上实现斐波那契算法。当按钮被点击时,简单地调用斐波那契函数。
子组件 Spinner.vue
:
<template>
<div class="flex justify-center items-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped></style>
父组件 WebAssemblySingle.vue
:
<template>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button @click="handleCalculate"
class="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition">
计算斐波那契数列
</button>
<Spinner v-if="isLoading" />
<p v-else class="text-xl">结果: {{ result }}</p>
</div>
</template>
<script lang="ts" setup>
import Spinner from '@/components/Spinner.vue';
import { ref } from 'vue';
const result = ref<number | null>(null);
const isLoading = ref(false);
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
const handleCalculate = () => {
isLoading.value = true;
const fibonacciResult = calculateFibonacci(42);
result.value = fibonacciResult;
isLoading.value = false;
};
</script>
<style scoped></style>
现在,让我们尝试点击 “计算斐波那契数列” 按钮,同时测量性能。要测量代码的性能,我们可以使用 Chrome DevTools
中的性能工具。
如您在界面中所见,我们的加载动画按钮甚至没有出现,取而代之的是突然显示了计算结果。从性能工具中我们也可以看到,由于斐波那契算法在主线程上的繁重计算,旋转动画被阻塞了大约 2.11 秒。
多线程(Web Worker
)
将繁重的计算从主线程移开的常用方法是使用 Web Worker
。
/**
* 将斐波那契算法移到 Web Worker 中
*/
self.addEventListener("message", function (e) {
const n = e.data;
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const result = fibonacci(n);
self.postMessage(result);
});
子组件 Spinner.vue
保持不变,父组件为 WebAssemblyWorker.vue
:
<template>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button @click="handleCalculate"
class="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition">
计算斐波那契数列
</button>
<Spinner v-if="isLoading" />
<p v-else class="text-xl">结果: {{ result }}</p>
</div>
</template>
<script lang="ts" setup>
import Spinner from '@/components/Spinner.vue';
import { ref } from 'vue';
const result = ref<number | null>(null);
const isLoading = ref(false);
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
const handleCalculate = () => {
isLoading.value = true;
const fibonacciResult = calculateFibonacci(42);
result.value = fibonacciResult;
isLoading.value = false;
};
</script>
<style scoped></style>
现在,如果我们进行性能测试,可以看到加载动画平稳运行。这是因为我们将繁重的计算任务移到了工作线程,避免了阻塞主线程。
可以看到,单线程和工作线程的计算时间都大约是 2 秒。那么问题来了,我们如何进一步优化呢?答案是使用 WebAssembly
。
图解说明:动画正在正常运行,此时斐波那契算法已在另一个线程上执行,有效避免了主线程的阻塞。
WebAssembly
— AssemblyScript
作为一名前端工程师,若在其他语言上的经验有限并想尝试 WebAssembly
,我们通常会选择 AssemblyScript
,因为它的开发体验最接近 TypeScript
。
以下是用 AssemblyScript
编写的等效斐波那契代码。
export function fibonacci(n: i32): i32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
编译该代码后,会生成一个 release.wasm
文件。然后我们可以在 JavaScript
代码中使用这个 Wasm
文件。
子组件 Spinner.vue
保持不变,父组件为 AssemblyScript.vue
:
<template>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button @click="handleCalculate"
class="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition">
计算斐波那契数列
</button>
<Spinner v-if="isLoading" />
<p v-else class="text-xl">结果: {{ result }}</p>
</div>
</template>
<script lang="ts" setup>
import Spinner from '@/components/Spinner.vue';
import { ref } from 'vue';
const result = ref<number | null>(null);
const isLoading = ref(false);
const handleCalculate = async () => {
isLoading.value = true;
const wasmUrl = new URL('@/assembly/release.wasm', import.meta.url);
const wasmModule = await fetch(wasmUrl);
const buffer = await wasmModule.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const wasm = module.instance.exports;
result.value = wasm.fibonacci(42);
isLoading.value = false;
};
</script>
<style scoped></style>
现在,如果我们再次测量性能,尽管仍在主线程上,但加载动画出现了,并且没有被繁重的计算阻塞。斐波那契算法现在大约耗时 830 毫秒,比仅使用 JavaScript
快了约 60%。
WebAssembly
— Rust
Rust
是 WebAssembly
的热门选择之一,Mozilla
的官方文档也推荐了它。让我们尝试用 Rust
实现相同的斐波那契算法。
use wasm_bindgen::prelude::*;
// 通过 WebAssembly 将函数暴露给 JavaScript
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
子组件 Spinner.vue
保持不变,父组件为 AssemblyScript.vue
:
<template>
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button @click="handleCalculate"
class="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition">
计算斐波那契数列
</button>
<Spinner v-if="isLoading" />
<p v-else class="text-xl">结果: {{ result }}</p>
</div>
</template>
<script lang="ts" setup>
import Spinner from '@/components/Spinner.vue';
import { ref } from 'vue';
import init, { fibonacci } from '@/assembly/pkg/hello_wasm.js';
const result = ref<number | null>(null);
const isLoading = ref(false);
const handleCalculate = async () => {
isLoading.value = true;
await init();
result.value = fibonacci(42);
isLoading.value = false;
};
</script>
<style scoped></style>
现在,让我们看看使用 Rust
编写的 WebAssembly
的效果。我们仍然在主线程上运行,但使用了 Wasm
。和 AssemblyScript
类似,即使在主线程上运行 Wasm
,加载动画仍然可以正常显示,不会被阻塞。令人惊讶的是,这个繁重的计算现在仅耗时 490 毫秒,比仅使用 JavaScript
快了 76%。
总结
-
繁重的计算会阻塞主线程并停止所有动画。
-
可以使用
Web Worker
将繁重计算移到后台线程。 -
通过将计算逻辑重写为
WebAssembly
可以进一步提升性能。使用斐波那契算法作为案例,我们获得了以下结果:JavaScript
:2秒WebAssembly
—AssemblyScript
:830毫秒(比JavaScript
快 60%)WebAssembly
—Rust
:490 毫秒(比JavaScript
快 76%)
备注:如果您对本文中的完整代码有兴趣,可以在评论区留言,或通过私信等方式联系我获取。