- 作者:老汪软件技巧
- 发表时间:2024-11-10 04:00
- 浏览量:
了解如何通过 Web Workers 和 WebAssembly 显著提升 JavaScript 应用的性能,并使用斐波那契算法作为案例分析。
JavaScript 通常运行在单线程上,通常称为“主线程”。这意味着 JavaScript 以同步方式一次执行一个任务。主线程还负责渲染任务,例如页面绘制和布局,以及处理用户交互,这也意味着长时间运行的 JavaScript 任务会导致浏览器变得无响应。这就是为什么当一个耗时的 JavaScript 函数运行时,网页可能会“卡住”,阻碍用户的正常操作。
我们将通过模拟斐波那契算法的繁重计算来演示如何阻塞主线程,并将通过以下几种方法来解决主线程阻塞的问题:
斐波那契算法
在本文的所有案例研究中,我们将使用一个简单且非常常见的斐波那契算法,其时间复杂度为 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 = refnull >(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 = refnull >(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 = refnull >(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 = refnull >(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%。
总结
备注:如果您对本文中的完整代码有兴趣,可以在评论区留言,或通过私信等方式联系我获取。