前言
我们平时开发的过程中,常常会遇到性能优化的要求。而对于前端的性能优化,我理解为有两大类,一类是网络层面的优化,即加速你的资源加载;另一类也是代码层面上的优化,即根据不同的业务场景写出更高性能的代码。
网络层面上的优化常常令人体验最深,想象一下你把站点的包从 10M
压缩到了 1M
,那么用户打开页面的时间也会大大缩短,相比这样用户的体验也会更好。
本文会以加载 FFmpeg.wasm
为例,阐述在实际项目中遇到大体积的包时如何优化加载速度。
初探
开始之前,先来介绍一下 FFmpeg.wasm
是什么。 FFmpeg.wasm
使用 WebAssembly
技术将 FFmpeg
的功能集成到 Web
应用程序中,使开发者能够在浏览器环境中处理音频和视频。
它的一些关键特点和用途如下:
- 多媒体处理:
FFmpeg.wasm
允许在浏览器中进行媒体处理,如音频和视频的解码、编码、剪辑、合成、添加字幕等操作。 - 转码: 可以在
Web
应用程序中实现实时的音视频转码 - 独立性:
FFmpeg.wasm
可以独立运行,不需要服务器端的支持,因此可以直接在客户端进行媒体处理,降低服务器负担。
简要来说 FFmpeg
是一个处理音视频的库,常规的音视频处理任务常常放在服务端执行,这些任务十分耗费服务端的 CPU
、内存资源。得益于 WebAssembly
, FFmpeg
可以移植到浏览器端使用——即 FFmpeg.wasm
,也就是说这些耗费资源的任务可以放在客户端去执行,也无疑是帮我们省掉了很多服务端资源。
在它的使用文档中,我们发现了这么一句话。
我把这个文件下载了下来,果真是超过30M的体积
也就是说加载这个库时我们需要加载将近 30M
的资源,假设我们的服务器下行带宽是 4M
,在用户能跑满这个带宽的情况下,那么加载这个库大概需要 60秒
左右。如果每一次进来都要等待加载 60秒
的话,那用户估计早就受不了。
如果你的团队里有 FFmpeg
的大佬,那么可以根据你们业务的要求去裁剪一个 FFmpeg
,这样的包体积应该会减少不少。本文还是会采用一些常规的思路去做优化,即压缩与缓存。
PS:如果你是 vite
用户,在跑上面的官方 demo
时遇到了这个报错的话,请把这段配置加到你的 vite
配置文件中。
optimizeDeps: { exclude: ["@ffmpeg/ffmpeg"], },
压缩
在现代化构建工具中,我们会发现打包出来的产物中往往存在 .gz
后缀名的这种类型的产物。它就是今天我们需要介绍的主角—— gzip
压缩。
gzip
使用 DEFLATE
算法进行数据压缩。 DEFLATE
算法是一种无损数据压缩算法,它基于 霍夫曼编码
和 LZ77
算法的组合。压缩后的文件通常以 .gz
作为扩展名。 gzip
可以压缩单个文件或多个文件,并将它们打包成一个压缩文件。
压缩文件可以使用gzip filename
命令,默认情况下, gzip
在压缩文件时会不保留原始文件,可以加上 -k
选项可以防止删除原始文件, gzip
支持不同的压缩级别,通过指定 -1
到 -9
之间的数字来调整。级别越高,压缩比越高,但耗费的时间也越多。解压文件则可以使用:gunzip filename.gz
或 gzip -d filename.gz
命令。
接下来就可以使用 gzip
压缩去压缩我们的 wasm
文件,即 gzip -9 ffmpeg-core.wasm
,压缩后的文件体积降到了原文件体积的 1/3
左右。
我使用的服务器是 nginx
,要在 nginx
中启用 gzip
压缩,需要填入以下配置:
http{ gzip on; gzip_comp_level 9; gzip_min_length 1100; gzip_buffers 16 8k; gzip_proxied any; gzip_types application/wasm; }
解释一下上面配置的参数:
这段 nginx
配置主要用于启用 gzip
压缩,并针对一些特定设置进行了配置。以下是对每个配置指令的解释:
gzip on;
:启用gzip
压缩功能gzip_comp_level 9;
:设置gzip
压缩级别,范围为1
到9
gzip_min_length 1100;
:设置启用压缩的最小文件长度,这里是1100
字节gzip_buffers 16 8k;
:设置用于gzip
压缩的内存缓冲区数量和大小,指定了16
个内存缓冲区,每个缓冲区大小为8k
。这样可以用来调整压缩时的内存使用情况。gzip_proxied any;
:设置在响应代理请求时启用gzip
压缩gzip_types application/wasm;
:指定需要进行gzip
压缩的MIME
类型
当启用 gzip
压缩时, nginx
会检查是否存在预先压缩过的.gz
文件。如果存在,它会直接提供这个预先生成的.gz
文件。如果不存在, nginx
会尝试动态地压缩内容,并将压缩后的内容发送给客户端。
为了减少服务器的开销,我这里把提前压缩好的 .gz
文件放到了 nginx
中,前端请求的时候会直接请求压缩好的文件。前端请求的是 gz
文件,但是对于 nginx
的响应来说,我们希望响应的 content-type
是 application/wasm
,所以这里还需要有一些额外的配置。
首先我们可以看到nginx的配置文件中使用了 include mime.types;
这个来配置 nginx
所能识别的 mime
类型。这个时候你可以查看跟 nginx
配置同目录的 mime.types
文件,看看这个文件中是否包含application/wasm wasm;
这一项配置,如果没有的话,我们得手动把它加上,不然 nginx
是不认识这个 mine
类型的,到时候传输的过程中会把它当成application/octet-stream
来处理,这样前端接收到的数据是无法正常使用的。
其次是我们在请求 wasm.gz
的时候,需要告诉 nginx
,我虽然请求的是一个 .gz
文件,但是我希望你返回的 content-type
是 wasm
所对应的类型。因此可以加上如下配置:
location /your-path/ { index index.html index.htm; location ~ \.wasm\.gz$ { types { application/wasm wasm; } default_type application/wasm; } }
前端请求代码也需要做出如下修改:
await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm.gz`, "application/wasm"), });
可以看到经过我们一顿操作猛如虎的配置之后, nginx
已经是可以正确传输压缩过后的文件
demo
代码也是可以正确的跑起来的
缓存
聊完压缩之后,可以再看一下缓存。如果你使用的是 nginx
,它会默认启动协商缓存,响应头如下:
虽然计算 ETag
可能会在一定程度上增加服务器的计算负担,不过在现代网络硬件架构下,这一成本通常相对较小了。但这里还是希望尝试给出另外一种缓存的解决思路——把加载好的文件存入 indexDB
,后续直接从前端缓存中获取,这样做的好处还是从一定程度上减轻服务端的压力。
这里用到了 localforage
来处理缓存的存取,整体思路是先看缓存有没有,如果有,直接返回,如果没有则去读取网络资源,然后写缓存。
nginx
传输 gz
资源的时候可能会开启分段传输,所以我们需要写一段代码来收集传输回来的所有片段。
const loadFFmpegCore = async () => { const cache = await localforage.getItem(FFMPEG_CACHE_KEY); console.log("cache", cache); if (cache) { console.log("load from cache"); return URL.createObjectURL(cache); } try { const response = await fetch(FFMPEG_CORE_PATH); if (!response.ok) { throw new Error(`load failed`); } let receivedLength = 0; const chunks = []; const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); receivedLength += value.length; } // 合并所有 chunks const arrayBuffer = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { arrayBuffer.set(chunk, position); position += chunk.length; } // 处理 arrayBuffer console.log("arrayBuffer", arrayBuffer); const blob = new Blob([arrayBuffer], { type: "application/wasm" }); await localforage.setItem(FFMPEG_CACHE_KEY, blob); return URL.createObjectURL(blob); } catch (error) { console.error("Error fetching data:", error); } };
使用到的地方也需要相应改一下:
await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), wasmURL: await loadFFmpegCore(), });
可以看到当我们后续加载的时候,就直接从缓存里拿了。
最后
以上就是本文对于前端大体积文件加载的全部内容,主要还是围绕压缩与缓存展开。如果你觉得有意思的话,点点关注点点赞吧~