Rust vs JavaScript:使用 WebAssembly 实现 76% 的性能提升

了解如何通过 Web WorkersWebAssembly 显著提升 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 = 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>

现在,如果我们进行性能测试,可以看到加载动画平稳运行。这是因为我们将繁重的计算任务移到了工作线程,避免了阻塞主线程。

Worker

可以看到,单线程和工作线程的计算时间都大约是 2 秒。那么问题来了,我们如何进一步优化呢?答案是使用 WebAssembly

Worker

图解说明:动画正在正常运行,此时斐波那契算法已在另一个线程上执行,有效避免了主线程的阻塞。

WebAssemblyAssemblyScript

作为一名前端工程师,若在其他语言上的经验有限并想尝试 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%。

AssemblyScript

AssemblyScript

WebAssemblyRust

RustWebAssembly 的热门选择之一,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%。

Rust

在这里插入图片描述

总结

  • 繁重的计算会阻塞主线程并停止所有动画。

  • 可以使用 Web Worker 将繁重计算移到后台线程。

  • 通过将计算逻辑重写为 WebAssembly 可以进一步提升性能。使用斐波那契算法作为案例,我们获得了以下结果:

    • JavaScript:2秒
    • WebAssemblyAssemblyScript:830毫秒(比 JavaScript 快 60%)
    • WebAssemblyRust:490 毫秒(比 JavaScript 快 76%)

备注:如果您对本文中的完整代码有兴趣,可以在评论区留言,或通过私信等方式联系我获取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一点一木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值