前端需要加载一个大体积的文件时,可以这么优化

公众号:【可乐前端】,分享前端面试与web实战知识

前言

我们平时开发的过程中,常常会遇到性能优化的要求。而对于前端的性能优化,我理解为有两大类,一类是网络层面的优化,即加速你的资源加载;另一类也是代码层面上的优化,即根据不同的业务场景写出更高性能的代码。

网络层面上的优化常常令人体验最深,想象一下你把站点的包从 10M 压缩到了 1M ,那么用户打开页面的时间也会大大缩短,相比这样用户的体验也会更好。

本文会以加载 FFmpeg.wasm 为例,阐述在实际项目中遇到大体积的包时如何优化加载速度。

初探

开始之前,先来介绍一下 FFmpeg.wasm 是什么。 FFmpeg.wasm 使用 WebAssembly 技术将 FFmpeg 的功能集成到 Web 应用程序中,使开发者能够在浏览器环境中处理音频和视频。

它的一些关键特点和用途如下:

  • 多媒体处理: FFmpeg.wasm 允许在浏览器中进行媒体处理,如音频和视频的解码、编码、剪辑、合成、添加字幕等操作。
  • 转码: 可以在 Web 应用程序中实现实时的音视频转码
  • 独立性: FFmpeg.wasm 可以独立运行,不需要服务器端的支持,因此可以直接在客户端进行媒体处理,降低服务器负担。

简要来说 FFmpeg 是一个处理音视频的库,常规的音视频处理任务常常放在服务端执行,这些任务十分耗费服务端的 CPU 、内存资源。得益于 WebAssemblyFFmpeg 可以移植到浏览器端使用------即 FFmpeg.wasm ,也就是说这些耗费资源的任务可以放在客户端去执行,也无疑是帮我们省掉了很多服务端资源。

在它的使用文档中,我们发现了这么一句话。

我把这个文件下载了下来,果真是超过30M的体积

也就是说加载这个库时我们需要加载将近 30M 的资源,假设我们的服务器下行带宽是 4M ,在用户能跑满这个带宽的情况下,那么加载这个库大概需要 60秒 左右。如果每一次进来都要等待加载 60秒 的话,那用户估计早就受不了。

如果你的团队里有 FFmpeg 的大佬,那么可以根据你们业务的要求去裁剪一个 FFmpeg ,这样的包体积应该会减少不少。本文还是会采用一些常规的思路去做优化,即压缩与缓存。

PS:如果你是 vite 用户,在跑上面的官方 demo 时遇到了这个报错的话,请把这段配置加到你的 vite 配置文件中。

js 复制代码
optimizeDeps: {
    exclude: ["@ffmpeg/ffmpeg"],
},

压缩

在现代化构建工具中,我们会发现打包出来的产物中往往存在 .gz 后缀名的这种类型的产物。它就是今天我们需要介绍的主角------ gzip 压缩。

gzip 使用 DEFLATE 算法进行数据压缩。 DEFLATE 算法是一种无损数据压缩算法,它基于 霍夫曼编码LZ77 算法的组合。压缩后的文件通常以 .gz 作为扩展名。 gzip 可以压缩单个文件或多个文件,并将它们打包成一个压缩文件。

压缩文件可以使用gzip filename命令,默认情况下, gzip 在压缩文件时会不保留原始文件,可以加上 -k 选项可以防止删除原始文件, gzip 支持不同的压缩级别,通过指定 -1-9 之间的数字来调整。级别越高,压缩比越高,但耗费的时间也越多。解压文件则可以使用:gunzip filename.gzgzip -d filename.gz 命令。

接下来就可以使用 gzip 压缩去压缩我们的 wasm 文件,即 gzip -9 ffmpeg-core.wasm ,压缩后的文件体积降到了原文件体积的 1/3 左右。

我使用的服务器是 nginx ,要在 nginx 中启用 gzip 压缩,需要填入以下配置:

ini 复制代码
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 压缩级别,范围为 19
  • 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-typeapplication/wasm ,所以这里还需要有一些额外的配置。

首先我们可以看到nginx的配置文件中使用了 include mime.types; 这个来配置 nginx 所能识别的 mime 类型。这个时候你可以查看跟 nginx 配置同目录的 mime.types 文件,看看这个文件中是否包含application/wasm wasm;这一项配置,如果没有的话,我们得手动把它加上,不然 nginx 是不认识这个 mine 类型的,到时候传输的过程中会把它当成application/octet-stream来处理,这样前端接收到的数据是无法正常使用的。

其次是我们在请求 wasm.gz 的时候,需要告诉 nginx ,我虽然请求的是一个 .gz 文件,但是我希望你返回的 content-typewasm 所对应的类型。因此可以加上如下配置:

bash 复制代码
location /your-path/ {
    index index.html index.htm;

    location ~ \.wasm\.gz$ {
        types {
            application/wasm     wasm;
        }
        default_type application/wasm;
    }
}

前端请求代码也需要做出如下修改:

js 复制代码
    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 资源的时候可能会开启分段传输,所以我们需要写一段代码来收集传输回来的所有片段。

js 复制代码
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);
  }
};

使用到的地方也需要相应改一下:

js 复制代码
await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
      wasmURL: await loadFFmpegCore(),
    });

可以看到当我们后续加载的时候,就直接从缓存里拿了。

最后

以上就是本文对于前端大体积文件加载的全部内容,主要还是围绕压缩与缓存展开。如果你觉得有意思的话,点点关注点点赞吧~

相关推荐
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄5 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui
尝尝你的优乐美7 小时前
vue3.0中h函数的简单使用
前端·javascript·vue.js
windy1a7 小时前
【C语言】js写一个冒泡顺序
javascript