❝在你没崛起之前,脸是用来丢的
❞
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust
及AI
应用知识分享」的Coder
❝此篇文章所涉及到的技术有
❞
WebAssembly
Rust
Web Worker
(comlink
)wasm-pack
Photon
ffmpeg.wasm
脚手架生成前端项目
因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。
前言
说起,「前端性能优化」,大家可能第一时间就会从网络/资源加载/压缩资源
等角度考虑。
正如下面所展示的一样。
上面所列的措施,是我们常规优化方案。针对上面的内容我们有机会来讲讲该如何做。
而今天呢,我们和大家唠唠利用WebAssembly
来优化前端渲染链路或者针对关键节点进行调优处理。
好了,天不早了,干点正事哇。
我们能所学到的知识点
❝❞
WebAssembly是个啥? 项目初始化&配置 Rust项目初始化 处理耗时任务 图像处理 优化音视频 优化游戏体验
1. WebAssembly是个啥?
之前,我们在浏览器第四种语言-WebAssembly已经对WebAssembly
有过介绍,为了行文的完整,我们再用简短的内容解释一下它。
WebAssembly
是一种「二进制指令格式」,旨在在浏览器中高效执行。
-
它 「作为JavaScript的补充」,允许我们用 Rust
、C++
和C
等语言编写性能关键代码,并在浏览器中运行(还记得我们前几天的文章 Rust 赋能前端 -- 写一个 File 转 Img 的功能分别讲了将C/Rust
编写成wasm
用于文档解析)。 -
通过将代码编译成 Wasm
,它变得 「平台无关」,并且可以以接近本地的速度运行。 -
Rust
是一种以安全性和性能著称的系统编程语言,由于其强大的保证和与Wasm
的无缝集成,已经在WebAssembly
生态系统中获得了广泛的关注。(如果想了解更多Rust
相关内容,可以参考我们的 Rust学习笔记系列文章) -
WebAssembly
为网络开发开辟了新的可能性,在一些复杂任务如游戏引擎、图像处理等方面有着显著的性能提升。
WebAssembly 的优势
WebAssembly
的一个最具说服力的特点是其在「计算密集型任务」中的性能提升。例如,在对庞大数据集进行复杂的统计计算时,WebAssembly
可能比常规的 JavaScript
快得多。这是因为 WebAssembly
的高度优化设计使得代码执行速度远远超过 JavaScript
。
WebAssembly
的另一个优点是其「可移植性」。跨平台应用程序的开发变得非常简单,因为可以从多种语言生成 WebAssembly
代码,并在任何平台上执行。
最后,「安全性」也是 WebAssembly
架构中的一个重要考虑因素。由于 WebAssembly
提供了沙箱执行环境,代码无法访问敏感数据或运行恶意代码。
下面是了解和学习WebAssembly
的RoadMap
。
2. 项目初始化&配置
进入正题之前,我们还是和之前一样,使用我们自己的脚手架-f_cli_f[1]构建一个以Vite
为打包工具的前端项目。
在本地合适的目录下执行如下代码:
npx f_cli_f create wasm_preformance
然后,我们在pages
中新建如下的目录结构
其中wasm
存放的是我们已经构建好的wasm
的资源。
配置Web Worker
由于我们在项目中会用到Web Worker
,所以我们还需要对其做一定的配置。之前呢,我们在React中使用多线程—Web Worker中介绍过,如何在React+Vite
的项目中使用Web Worker
。
而今天,我们再介绍另外一种更加优雅的方式 - Comlink[2]。
❝❞
Comlink
是一个由Google Chrome Labs
开发的轻量级库,它旨在简化Web Worker
与主线程之间的通信,让我们能够充分利用多线程处理的威力,提升前端应用性能。
由于,我们是用Vite
搭建的前端项目,所以我们还需要在项目中借助vite-plugin-comlink[3]。
我们可以通过如下代码安装对应的依赖。
yarn add -D vite-plugin-comlink
yarn add comlink
然后,将对应的库配置到vite.config.js
中。
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
的相关内容,可以看我们之前的文章❞
Web性能优化之Worker线程(上) Web性能优化之Worker线程(下) React中使用多线程—Web Worker
配置WebAssembly
如果看过我们之前的文章(Rust 赋能前端 -- 写一个 File 转 Img 的功能)就对这块不会陌生。
在Vite
项目中使用WebAssembly
我们需要配置vite-plugin-wasm[4]和vite-plugin-top-level-await[5]
然后,也是需要在vite.config.js
的plugin
和worker
中进行相关处理。这里就不展开说明了。之前的文章有过解释。
3. Rust项目初始化
在讲项目页面结构时说过,我们在组件目录中特意有一个wasm
目录用于存放编译好的wasm
信息。
我们选择wasm
代码和前端项目分离的方式,也就是我们会重新启动一个Rust
项目。
通过如下代码在合适的文件目录下执行。
cargo new --lib rust_comformation2web
然后,因为我们想要把Rust
编译成wasm
并且还需要操作对应的dom
等。所以,我们需要按照对应的crate
。
安装依赖
所以,我们来更新对应的Cargo.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
的配置如下:
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
新增两个命令
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
我们就可以通过yarn serve
查看效果亦或者yarn build
执行对应的rust
打包。
能够实现这一切的功劳都是-@wasm-tool/wasm-pack-plugin[6]所赐予的。
编译处理
❝但是呢,我们对
❞Rust
编译处理不使用之前的yarn build
,而是使用cargo
自己的构建工具 - wasm-pack[7]
wasm-pack build --target web --release
如果一切都正常的话,对应的wasm
就会被打包到pkg
文件夹下面了。
然后,我们就可以将所有文件复制到Vite
项目中的wasm/xx
目录下。
最后,我们就可以在React
组件中通过
import init, { fib } from './wasm/xx';
引入对应的wasm
函数了。
❝前面铺垫了那么多,其实为了更好的讲下面的内容,我们先把一些和逻辑代码不相关的配置内容提前介绍了,这样我们就可以将更过的注意力放在代码实现上了。
❞
4. 处理耗时任务
先说结果
当执行一个处理耗时任务
时,WebAssembly/JS WebWorker/JS主线程
三者的执行时间是由低到高
排列的。
❝❞
WebAssembly
<JS WebWorker
<JS主线程
针对上面的我们有几点需要注意
-
JS WebWorker
针对JS主线程
优化率不是很高,(有时候worker
执行时间甚至比JS主线程
长) -
WebAssembly
通过至极的内存优化,还可以将优化率提高到50%
以上。
听我解释
我们都知道JS
是单进程的,所以我们在处理一些处理耗时任务
就会很吃力。当然,我们也可以借助Web Worker
来开启新的子线程来缓解主线程的计算压力。但是,在一些计算量特别大的功能面前,一切的计算都是收效甚微的。
其实,将一些处理耗时任务
放置到Web Worker
中只是不想让耗时任务
过多的占用主线程资源,从而让页面没有卡顿的感觉。这就是大家所熟悉的浏览器在 1 秒钟内完成 60 次图像的绘制,用户才会感觉页面顺畅
。
❝关于浏览器渲染的相关内容,可以看我们之前的文章
❞
浏览器工作原理 [8] 页面是如何生成的(宏观角度) 像素是怎样练成的
为了在前端环境模拟处理耗时任务
,我们采用在前端环境中执行一个fibonacci
的计算过程。
在WasmPerformance
的index.tsx
中有如下的页面操作。
也就是说,我们在JS主线程/JS WebWorker/WebAssembly
中分别执行一个耗时的fibonacci
。
我们在tool.ts
中构建了一个最简单的fibonacci
函数。
function fibJS(n: number): number {
if (n < 2) {
return n;
}
return fibJS(n - 1) + fibJS(n - 2);
}
对应的页面代码如下
从上面我们看到几个关键的点
我们用state
来维护计算的结果
和时间
。
const [calculateInfo, setCalculateInfo] = useState<CalculateInfo>({
js: { result: 0, executionTime: 0 },
wasm: { result: 0, executionTime: 0 },
webworker: { result: 0, executionTime: 0 },
});
然后,我们在handleCalculate
中执行不同的操作逻辑。
其中measureExecutionTime
是我们在tool
定义的用于检测指定函数被执行时的所用时间的函数.
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
代码如下:
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. 图像处理
先说结果
我们写了两个示例
-
将指定文本信息绘制到图片上 -
将特定图形绘制到图片上
无论是哪种情况,我们可以得出一个比较明显的情况。
❝在图像处理的部分功能点上,
❞WebAssembly
的性能远高于JS
因为,我们这里没做WebAssembly
的内存优化,当处理数据「超级大」时,由于数据传输的问题,反而WebAssembly
的执行时间会比JS
长。但是呢,这块不在我们的讨论范围内。后期有机会写相关的文章。
下面,我们就按照上面的示例来分别讲讲它们的代码实现。有些代码的逻辑其实很简单,我们已经有对应的注释,所以也不会用多余的篇幅解释。
绘制文本到图片上
对应的页面结构如下
我们还是用了一个state
来维护状态信息。
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
.
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
中。
然后我们通过如下代码
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[9]。
❝❞
Photon
是一个高性能的图像处理库,用Rust
编写并可编译为WebAssembly
,既可以在本地使用Web
也可以在Web
上使用。
这是它能做相关功能
6. 优化音视频
写到这里呢,我们就不在罗列相关代码了。所以,我们给出一些针对音视频的优化的解决方案。(当然,我们后期也会有专门的文章)
在这里我们介绍一种wasm
库-ffmpeg.wasm[10]
❝❞
ffmpeg.wasm
是 FFmpeg[11] 的针对WebAssembly
/JavaScript
端口,支持在浏览器中录制、转换和流式传输视频和音频。它利用Emscripten
来转译FFmpeg
源代码和许多库得到
具体的功能和库如下:
7. 优化游戏体验
得益于WebAssembly
极致的内存管理,然后其二进制特性,WebAssembly
提供接近本地执行速度的性能,使得复杂的游戏逻辑和高帧率的图形渲染可以在浏览器中高效运行。
还得之前我们写过Game = Rust + WebAssembly + 浏览器
还有,如果我们想要更多的效果,我们可以选择使用bevy[12] - 一款基于Rust的数据驱动的游戏引擎。
然后我们还在itch.io[13]查看哪些游戏是用Rust
写的。
后记
「分享是一种态度」。
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」
Reference
f_cli_f: https://www.npmjs.com/package/f_cli_f
[2]Comlink: https://github.com/GoogleChromeLabs/comlink
[3]vite-plugin-comlink: https://www.npmjs.com/package/vite-plugin-comlink
[4]vite-plugin-wasm: https://www.npmjs.com/package/vite-plugin-wasm
[5]vite-plugin-top-level-await: https://www.npmjs.com/package/vite-plugin-top-level-await
[6]@wasm-tool/wasm-pack-plugin: https://www.npmjs.com/package/@wasm-tool/wasm-pack-plugin
[7]wasm-pack: https://github.com/rustwasm/wasm-pack
[8]浏览器工作原理: https://juejin.cn/post/6923953599936954382
[9]Photon: https://github.com/silvia-odwyer/photon
[10]ffmpeg.wasm: https://ffmpegwasm.netlify.app/
[11]FFmpeg: https://www.ffmpeg.org/
[12]bevy: https://bevyengine.org/
[13]itch.io: https://itch.io/games/html5/made-with-rust
本文由 mdnice 多平台发布