这是一张插图,展示了 JavaScript 与 AssemblyScript 以及 Rust 与 WebAssembly 的比较。
JavaScript 通常在一个单线程上运行,这个线程也常被称为“主线程”。这意味着 JavaScript 一次只执行一个任务,并且是同步执行的。主线程还负责处理诸如绘制和布局等渲染任务,以及用户交互。因此,长时间运行的 JavaScript 任务会让浏览器变得无响应。这就是为什么当一个耗时的 JavaScript 函数运行时,网页会“卡顿”,导致用户无法进行交互。
我们将通过使用斐波那契算法来模拟繁重的计算,演示如何阻塞主线程,并将展示几种方法来解决这一问题,例如:
-
多线程(Web Worker 技术),
-
使用 AssemblyScript 编写的 WebAssembly,
- 使用 Rust 编写的 WebAssembly,
我们将使用简单且非常常见的斐波那契数列算法(时间复杂度为 O(2^n))来进行本文中的所有案例分析。
const calculateFibonacci = (n: number): number => {
如果 n 小于等于 1 返回 n;
返回 calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
单线程 (Single Thread)
接下来,让我们直接在主线程上实现斐波那契算法的代码。当点击按钮的时候,简单地调用一下斐波那契函数。
"use client";
import { useState } from "react";
/**
* 假装加载动画。
*/
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
const handleCalculate = () => {
setIsLoading(true);
/**
* 模拟长时间计算。
*/
const result = calculateFibonacci(42);
setResult(result);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
计算Fibonacci
</button>
{isLoading ? <Spinner /> : <p className="text-xl">结果是: {result}</p>}
</div>
);
}
现在,让我们尝试点击“计算斐波那契数列”按钮,同时测量其性能。要测量代码的性能,我们可以使用 Chrome DevTools 中的性能分析工具。
正如您在用户界面上看到的,我们的旋转加载按钮甚至没有出现,而是突然显示计算结果。我们也可以从性能工具中看到,我们的旋转动画被该繁重计算阻塞了大约 2.06秒(2秒零60毫秒)。
性能监控工具显示出主要线程大约被卡住了2秒左右。
使用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);
});
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
/**
* 而不是直接在主线程中运行斐波那契函数,
* 而是在Web工作线程中运行
*/
const handleCalculate = () => {
setIsLoading(true);
const worker = new Worker(
new URL("./fibonacci-worker.js", import.meta.url),
);
worker.postMessage(42);
worker.onmessage = (e) => {
setResult(e.data);
setIsLoading(false);
worker.terminate();
};
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
计算斐波那契数列
</button>
{isLoading ? <Spinner /> : <p className="text-xl">结果是: {result}</p>}
</div>
);
}
现在,如果我们尝试测量,spinner 的动画效果运行得很流畅。这是因为我们将繁重的计算移到了 worker 线程,避免了主线程被阻塞。
如你所见,不论是单线程还是多线程,计算都差不多需要2秒钟。接下来的问题就是,我们怎么才能改进这一点呢?答案就是通过使用WebAssembly。
性能监控工具表明,现在大量的计算任务正在工作进程中执行。
WebAssembly — 编译脚本 (WASM-编译脚本)作为一名前端工程师,虽然我只有一些其他语言的经验,但想要尝试WebAssembly时,我通常会选择AssemblyScript,因为它提供了最接近TypeScript的开发体验。
这里有一段用AssemblyScript编写的Fibonacci代码。
export function 斐波那契(n: i32): i32 {
if (n <= 1) return n;
return 斐波那契(n - 1) + 斐波那契(n - 2);
}
如果我们编译这段代码,将会生成一个 release.wasm
文件。之后我们就可以在我们的 JavaScript 代码库中使用这个 Wasm 文件了。
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleCalculate = async () => {
setIsLoading(true);
// 加载和实例化 WebAssembly 模块
const wasmModule = await fetch("/release.wasm");
const buffer = await wasmModule.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const wasm = module.instance.exports;
// 调用 WebAssembly 模块中的 Fibonacci 函数
const fibResult = wasm.fibonacci(42);
setResult(fibResult);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition: all"
>
计算斐波那契
</button>
{isLoading ? <Spinner /> : <p className="text-xl">结果是: {result}</p>}
</div>
);
}
现在,虽然我们仍然在主线程上,如果再次测量这个,加载动画会显示出来而不会被繁重的计算阻塞。斐波那契算法现在大约需要 950毫秒 的时间,这比仅使用JavaScript执行要快 53%。
性能工具表明这种语言 AssemblyScript 比 JavaScript 快 53% 的速度。
WebAssembly(网络装配)— Rust(Rust语言)一种在网页上运行代码的低级语言
Rust 是 WebAssembly 的热门选择之一,这一点在 Mozilla 的官方文档中也有提到。我们来用 Rust 实现一下同样的 Fibonacci 算法。
use wasm_bindgen::prelude::*;
// 通过 WebAssembly 把这个函数暴露给 JavaScript,这样 JavaScript 就可以调用了。
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
// 根据 n 的值来返回结果,如果 n 是 0,则返回 0;如果 n 是 1,则返回 1;否则计算 fibonacci(n - 1) 加上 fibonacci(n - 2) 的结果。
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleCalculate = async () => {
setIsLoading(true);
// 请使用真实的 wasm 文件
const wasmModule = await fetch("/pkg/rust_wasm_fibonacci_bg.wasm");
const buffer = await wasmModule.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const wasm = module.instance.exports;
// 调用 WebAssembly 模块中的 Fibonacci 函数
const fibResult = wasm.fibonacci(42); // 假设函数导出为 'fibonacci'
setResult(fibResult);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
计算斐波那契数
</button>
{isLoading ? <Spinner /> : <p className="text-xl">结果为: {result}</p>}
</div>
);
}
现在,我们来看看使用WebAssembly和Rust的效果。我们仍然使用主线程,但这次则是用到了Wasm。就像在AssemblyScript中一样,尽管我们在主线程上运行Wasm,加载动画依然显示,不会被卡住。令人惊讶的是,现在的复杂计算仅需684毫秒,比单纯使用JavaScript快了66%。
根据性能测试,Rust 比 JavaScript 快 66% 的速度。
简介和结论。
- 大量计算会卡住主线程并停止所有动画效果。
- 大量计算可以交给 Web Worker 处理。
- 使用 WebAssembly 重写逻辑可以优化大量计算。以斐波那契数列为例,我们得到了如下结果:
- JavaScript:2秒
- WebAssembly — AssemblyScript:953毫秒(比 JavaScript 快 53% 的时间)
- WebAssembly — Rust:684毫秒(比 JavaScript 快 66% 的时间)
参考