手记

Rust vs JavaScript:借助WebAssembly实现性能提升66%

了解 Web Workers 和 WebAssembly 如何通过以斐波那契算法为例显著提升你的 JavaScript 应用程序的性能。

这是一张插图,展示了 JavaScript 与 AssemblyScript 以及 Rust 与 WebAssembly 的比较。

JavaScript 通常在一个单线程上运行,这个线程也常被称为“主线程”。这意味着 JavaScript 一次只执行一个任务,并且是同步执行的。主线程还负责处理诸如绘制和布局等渲染任务,以及用户交互。因此,长时间运行的 JavaScript 任务会让浏览器变得无响应。这就是为什么当一个耗时的 JavaScript 函数运行时,网页会“卡顿”,导致用户无法进行交互。

我们将通过使用斐波那契算法来模拟繁重的计算,演示如何阻塞主线程,并将展示几种方法来解决这一问题,例如:

斐波那契算法

我们将使用简单且非常常见的斐波那契数列算法(时间复杂度为 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% 的时间)

参考

0人推荐
随时随地看视频
慕课网APP