宝贝,带上WebAssembly,换个姿势来优化你的前端应用

在你没崛起之前,脸是用来丢的

大家好,我是柒八九 。一个专注于前端开发技术/RustAI应用知识分享Coder

此篇文章所涉及到的技术有

  1. WebAssembly
  2. Rust
  3. Web Worker(comlink)
  4. wasm-pack
  5. Photon
  6. ffmpeg.wasm
  7. 脚手架生成前端项目

因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。


前言

说起,前端性能优化 ,大家可能第一时间就会从网络/资源加载/压缩资源等角度考虑。

正如下面所展示的一样。

上面所列的措施,是我们常规优化方案。针对上面的内容我们有机会来讲讲该如何做。

而今天呢,我们和大家唠唠利用WebAssembly来优化前端渲染链路或者针对关键节点进行调优处理。


好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. WebAssembly是个啥?
  2. 项目初始化&配置
  3. Rust项目初始化
  4. 处理耗时任务
  5. 图像处理
  6. 优化音视频
  7. 优化游戏体验

1. WebAssembly是个啥?

之前,我们在浏览器第四种语言-WebAssembly已经对WebAssembly有过介绍,为了行文的完整,我们再用简短的内容解释一下它。

WebAssembly是一种二进制指令格式,旨在在浏览器中高效执行。

  • 作为JavaScript的补充 ,允许我们用RustC++C等语言编写性能关键代码,并在浏览器中运行(还记得我们前几天的文章Rust 赋能前端 -- 写一个 File 转 Img 的功能分别讲了将C/Rust编写成wasm用于文档解析)。
  • 通过将代码编译成Wasm,它变得平台无关,并且可以以接近本地的速度运行。
  • Rust是一种以安全性和性能著称的系统编程语言,由于其强大的保证和与Wasm的无缝集成,已经在WebAssembly生态系统中获得了广泛的关注。(如果想了解更多Rust相关内容,可以参考我们的Rust学习笔记系列文章)
  • WebAssembly为网络开发开辟了新的可能性,在一些复杂任务如游戏引擎、图像处理等方面有着显著的性能提升。

WebAssembly 的优势

WebAssembly的一个最具说服力的特点是其在计算密集型任务 中的性能提升。例如,在对庞大数据集进行复杂的统计计算时,WebAssembly 可能比常规的 JavaScript 快得多。这是因为 WebAssembly 的高度优化设计使得代码执行速度远远超过 JavaScript

WebAssembly 的另一个优点是其可移植性 。跨平台应用程序的开发变得非常简单,因为可以从多种语言生成 WebAssembly 代码,并在任何平台上执行。

最后,安全性 也是 WebAssembly 架构中的一个重要考虑因素。由于 WebAssembly 提供了沙箱执行环境,代码无法访问敏感数据或运行恶意代码。

下面是了解和学习WebAssemblyRoadMap


2. 项目初始化&配置

进入正题之前,我们还是和之前一样,使用我们自己的脚手架-f_cli_f构建一个以Vite为打包工具的前端项目。

在本地合适的目录下执行如下代码:

lua 复制代码
npx f_cli_f create wasm_preformance

然后,我们在pages中新建如下的目录结构

其中wasm存放的是我们已经构建好的wasm的资源。

配置Web Worker

由于我们在项目中会用到Web Worker,所以我们还需要对其做一定的配置。之前呢,我们在React中使用多线程---Web Worker中介绍过,如何在React+Vite的项目中使用Web Worker

而今天,我们再介绍另外一种更加优雅的方式 - Comlink

Comlink是一个由Google Chrome Labs开发的轻量级库,它旨在简化Web Worker与主线程之间的通信,让我们能够充分利用多线程处理的威力,提升前端应用性能。

由于,我们是用Vite搭建的前端项目,所以我们还需要在项目中借助vite-plugin-comlink

我们可以通过如下代码安装对应的依赖。

shell 复制代码
yarn add -D vite-plugin-comlink
yarn add comlink

然后,将对应的库配置到vite.config.js中。

javascript 复制代码
import { comlink } from "vite-plugin-comlink";

export default {
  plugins: [comlink()],
  worker: {
    plugins: () => [comlink()],
  },
};

这里有一点需要额外注意,comlink要放置在plugins第一个位置。

针对TypeScript项目,我们还需要在vite-env.d.ts中新增/// <reference types="vite-plugin-comlink/client" />

然后我们就可以用优雅的方式来使用WebWorker了。

可以看到,使用了comlink后,我们在使用多线程能力时,不需要写那么多模板代码,而是通过Promise来接收从子线程返回的数据。

关于Web Worker的相关内容,可以看我们之前的文章

配置WebAssembly

如果看过我们之前的文章(Rust 赋能前端 -- 写一个 File 转 Img 的功能)就对这块不会陌生。

Vite项目中使用WebAssembly我们需要配置vite-plugin-wasmvite-plugin-top-level-await

然后,也是需要在vite.config.jspluginworker中进行相关处理。这里就不展开说明了。之前的文章有过解释。


3. Rust项目初始化

在讲项目页面结构时说过,我们在组件目录中特意有一个wasm目录用于存放编译好的wasm信息。

我们选择wasm代码和前端项目分离的方式,也就是我们会重新启动一个Rust项目。

通过如下代码在合适的文件目录下执行。

shell 复制代码
cargo new --lib rust_comformation2web

然后,因为我们想要把Rust编译成wasm并且还需要操作对应的dom等。所以,我们需要按照对应的crate

安装依赖

所以,我们来更新对应的Cargo.toml

toml 复制代码
[package]
name = "rust_comformation2web"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.92"
console_error_panic_hook = "0.1.7"
js-sys = "0.3.69"

[dependencies.web-sys]
version = "0.3.69"
features = [
    'Document',
    'TextMetrics',
    'CanvasRenderingContext2d',
    'HtmlCanvasElement',
    'Window'
]

然后,我们就可以在src/lib.rs写我们对应的代码了。

如果对自己的代码质量不是很放心,并且又不想写Test模块了,我们将Rust所在的文件目录,构建成一个Node项目(通过npm init),并配合对应的打包软件(Webpack)来直接验证wasm的效果。

对应的webpack.config.js的配置如下:

js 复制代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'index.js',
    },
    plugins: [
         new HtmlWebpackPlugin({
            template: 'index.html'
        }),
        new WasmPackPlugin({
            crateDirectory: path.resolve(__dirname, ".")
        }),
        // 让这个示例在不包含`TextEncoder`或`TextDecoder`的Edge浏览器中正常工作。
        new webpack.ProvidePlugin({
          TextDecoder: ['text-encoding', 'TextDecoder'],
          TextEncoder: ['text-encoding', 'TextEncoder']
        })
    ],
    mode: 'development',
    experiments: {
        asyncWebAssembly: true
   }
};

然后,我们在package.json新增两个命令

json 复制代码
"scripts": {
    "build": "webpack",
    "serve": "webpack serve"
  },

我们就可以通过yarn serve查看效果亦或者yarn build执行对应的rust打包。

能够实现这一切的功劳都是-@wasm-tool/wasm-pack-plugin所赐予的。

编译处理

但是呢,我们对Rust编译处理不使用之前的yarn build,而是使用cargo自己的构建工具 - wasm-pack

shell 复制代码
wasm-pack build --target web --release

如果一切都正常的话,对应的wasm就会被打包到pkg文件夹下面了。

然后,我们就可以将所有文件复制到Vite项目中的wasm/xx目录下。

最后,我们就可以在React组件中通过

csharp 复制代码
import init, { fib } from './wasm/xx';

引入对应的wasm函数了。


前面铺垫了那么多,其实为了更好的讲下面的内容,我们先把一些和逻辑代码不相关的配置内容提前介绍了,这样我们就可以将更过的注意力放在代码实现上了。


4. 处理耗时任务

先说结果

当执行一个处理耗时任务时,WebAssembly/JS WebWorker/JS主线程三者的执行时间是由低到高排列的。

WebAssembly < JS WebWorker<JS主线程

针对上面的我们有几点需要注意

  1. JS WebWorker针对JS主线程优化率不是很高,(有时候worker执行时间甚至比JS主线程长)
  2. WebAssembly通过至极的内存优化,还可以将优化率提高到50%以上。

听我解释

我们都知道JS是单进程的,所以我们在处理一些处理耗时任务就会很吃力。当然,我们也可以借助Web Worker来开启新的子线程来缓解主线程的计算压力。但是,在一些计算量特别大的功能面前,一切的计算都是收效甚微的。

其实,将一些处理耗时任务放置到Web Worker中只是不想让耗时任务过多的占用主线程资源,从而让页面没有卡顿的感觉。这就是大家所熟悉的浏览器在 1 秒钟内完成 60 次图像的绘制,用户才会感觉页面顺畅

关于浏览器渲染的相关内容,可以看我们之前的文章

为了在前端环境模拟处理耗时任务,我们采用在前端环境中执行一个fibonacci的计算过程。

WasmPerformanceindex.tsx中有如下的页面操作。

也就是说,我们在JS主线程/JS WebWorker/WebAssembly中分别执行一个耗时的fibonacci

我们在tool.ts中构建了一个最简单的fibonacci函数。

javascript 复制代码
function fibJS(n: number): number {
  if (n < 2) {
    return n;
  }
  return fibJS(n - 1) + fibJS(n - 2);
}

对应的页面代码如下

从上面我们看到几个关键的点

我们用state来维护计算的结果时间

typescript 复制代码
const [calculateInfo, setCalculateInfo] = useState<CalculateInfo>({
    js: { result: 0, executionTime: 0 },
    wasm: { result: 0, executionTime: 0 },
    webworker: { result: 0, executionTime: 0 },
  });

然后,我们在handleCalculate中执行不同的操作逻辑。

其中measureExecutionTime是我们在tool定义的用于检测指定函数被执行时的所用时间的函数.

typescript 复制代码
function measureExecutionTime<T extends (...args: any[]) => any>(
  fn: T
): (...args: Parameters<T>) => { result: ReturnType<T>; executionTime: number } {
  return function (...args: Parameters<T>): { result: ReturnType<T>; executionTime: number } {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    const executionTime = end - start;
    return { result, executionTime };
  };
}

还有,我们在handleCalculate在接收到type为3时,是触发了一个wasm版本的fibonacci函数。

由于,对应的Rust代码如下:

rust 复制代码
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib(n: usize) -> usize {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

而上面的Rust代码会通过wasm-pack build --target web --release进行打包处理,并且打包后的相关内容被复制到了前端项目中wasm/calculate

然后在组件中通过import init, { fib } from './wasm/calculate';方式来导入。


5. 图像处理

先说结果

我们写了两个示例

  1. 将指定文本信息绘制到图片上
  2. 将特定图形绘制到图片上

无论是哪种情况,我们可以得出一个比较明显的情况。

在图像处理的部分功能点上,WebAssembly的性能远高于JS

因为,我们这里没做WebAssembly的内存优化,当处理数据超级大 时,由于数据传输的问题,反而WebAssembly的执行时间会比JS长。但是呢,这块不在我们的讨论范围内。后期有机会写相关的文章。

下面,我们就按照上面的示例来分别讲讲它们的代码实现。有些代码的逻辑其实很简单,我们已经有对应的注释,所以也不会用多余的篇幅解释。

绘制文本到图片上

对应的页面结构如下

我们还是用了一个state来维护状态信息。

typescript 复制代码
 const [drawInfo, setDrawInfo] = useState<DrawInfo>({
    js: { url: '', executionTime: 0 },
    wasm: { url: '', executionTime: 0 },
    js_circle: { url: '', executionTime: 0 },
    wasm_circle: { url: '', executionTime: 0 },
  });

然后在handleDraw中处理事件逻辑。

其中drawTextToCanvas是利用JS来绘制文本到Canvas,而drawTextToCanvasWasm是利用wasm处理相关逻辑。

JS 版本的drawText

该函数定义在tool.ts中,然后就是接收一个String类型的数据,并将其渲染到Canvas中。

Rust 版本的drawText

然后,别忘记在头部引入对应的crate.

rust 复制代码
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
extern crate console_error_panic_hook;
use std::panic;

其实这块的逻辑,和之前我们讲的Rust 赋能前端 -- 写一个 File 转 Img 的功能的核心功能是类似的。

该函数通过wasm-pack编译到pkg中,然后我们复制对应的文件到React项目的wasm/draw中。

然后我们通过如下代码

javascript 复制代码
import init4Draw, {
  draw_text_to_canvas as drawTextToCanvasWasm,
  draw_circle_to_canvas as drawCircleToCanvasWasm,
} from './wasm/draw';

进行函数的导入。

绘制图形到图片上

对应的页面结构和事件回调和之前是类似的,我们就省略了这部分的解释。

JS 版本的drawCircle

该部分也是定义在tool.ts

Rust 版本的drawCircle

此函数的处理过程和drawText是一样的。

利用Photon操作图形

针对图片操作,不单单只有绘制文本/绘制图案,其实我们还可以做类似(裁剪/新增水印/图片翻转等)。

我们可以借助一些成熟的WebAssembly来做上述的操作。这里呢,给大家推荐一个库Photon

Photon 是一个高性能的图像处理库,用 Rust 编写并可编译为 WebAssembly,既可以在本地使用 Web 也可以在 Web 上使用。

这是它能做相关功能


6. 优化音视频

写到这里呢,我们就不在罗列相关代码了。所以,我们给出一些针对音视频的优化的解决方案。(当然,我们后期也会有专门的文章)

在这里我们介绍一种wasm库-ffmpeg.wasm

ffmpeg.wasmFFmpeg 的针对 WebAssembly / JavaScript 端口,支持在浏览器中录制、转换和流式传输视频和音频。它利用 Emscripten 来转译 FFmpeg 源代码和许多库得到

具体的功能和库如下:


7. 优化游戏体验

得益于WebAssembly极致的内存管理,然后其二进制特性,WebAssembly 提供接近本地执行速度的性能,使得复杂的游戏逻辑和高帧率的图形渲染可以在浏览器中高效运行。

还得之前我们写过Game = Rust + WebAssembly + 浏览器

还有,如果我们想要更多的效果,我们可以选择使用bevy - 一款基于Rust的数据驱动的游戏引擎。

然后我们还在itch.io查看哪些游戏是用Rust写的。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。

相关推荐
帅帅哥的兜兜19 分钟前
react中hooks使用
前端·javascript·react.js
吞掉星星的鲸鱼1 小时前
使用高德api实现天气查询
前端·javascript·css
lilye661 小时前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
zhougl9963 小时前
html处理Base文件流
linux·前端·html
花花鱼3 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_3 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
careybobo5 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
niandb5 小时前
The Rust Programming Language 学习 (九)
windows·rust
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue