一次大批量处理视频文件的性能优化记录

功能描述

这个功能是这样,有一个视频文件的管理页面,可以选择多个视频后,进行上传。问题出在了多个(100以上)视频文件选择的这一步,部分电脑可能会卡顿甚至卡死。

我顺着代码捋了一下,大概代码如下:

  1. 视频文件选择使用的组件是 element-plus 的 Upload 组件,可以通过 on-change 传入在文件发生变化后执行的方法。
  2. 在on-change中,通过 getFileMd5 获取文件的 MD5 信息;
  3. 在on-change中,通过 getVideoInfo 获取视频文件的时长、宽、高信息;

我现在自己的电脑上,通过浏览器的任务管理器看了一下, getFileMd5 和 getVideoInfo 都会造成浏览器的占用的CPU和内存过大的问题。

看下这两个方法是如何实现的:

js 复制代码
import SparkMD5 from 'spark-md5'

// 获取文件的 MD5
export const getFileMd5 = file => {
  return new Promise((resolve, reject) => {
    const spark = new SparkMD5.ArrayBuffer()
    const reader = new FileReader()

    reader.onload = function (e) {
      // 使用 SparkMD5 计算 md5
      spark.append(e.target.result) 
      const md5 = spark.end() // 完成计算
      resolve(md5)
    }

    reader.onerror = function (error) {
      reject(error)
    }

    // 读取文件为 ArrayBuffer
    reader.readAsArrayBuffer(file)
  })
}
js 复制代码
// 获取视频的时长、宽度和高度
export const getVideoInfo = file => {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file) // 创建一个指向该视频文件的 url
    const video = document.createElement('video')
    video.src = url

    video.onloadedmetadata = () => {
      resolve({ 
        duration: video.duration, // 获取时长
        width: video.videoWidth, // 获取宽度
        height: video.videoHeight // 获取高度
      })
      URL.revokeObjectURL(url) // 释放URL对象
    }

    video.load() // 开始加载视频文件
  })
}

优化 MD5 的获取方法

主要思路是:通过将文件拆分为多个块,最后再合并计算最终的 MD5。这样,即使处理大文件,也不会导致浏览器的占用过大。

js 复制代码
// 主要思路,将文件分多次 spark.append,避免一次性处理
const getFileMd5 = file => {
  return new Promise((resolve, reject) => {
    const chunkSize = 1024 * 1024 * 2; // 拆分的大小
    const spark = new SparkMD5.ArrayBuffer()
    const reader = new FileReader()
    let currentChunk = 0;
    const totalChunks = Math.ceil(file.size / chunkSize);

    function loadNextChunk() {
        const start = currentChunk * chunkSize;
        const end = Math.min(start + chunkSize, file.size);
        const chunk = file.slice(start, end); // 拆分文件

        reader.onload = (e) => {
            spark.append(e.target.result); 
            currentChunk++;
            // 如果未处理完,则继续处理
            if (currentChunk < totalChunks) {
                loadNextChunk(); 
            } else {
                resolve(spark.end()); // 输出 md5 值
            }
        };

        reader.onerror = (error) => {
            reject(error) ;
        };

        reader.readAsArrayBuffer(chunk); // Read the chunk
    }

    loadNextChunk(); 
  })
}

优化视频信息获取方法

目前没有什么特别好的方法,只能减少 url 创建的次数,减轻一点负担。改进后如下:

js 复制代码
const getVideoInfo = file => {
  return new Promise((resolve, reject) => {
    // 因为 element-plus 的 Upload 组件会默认生成 url,所以就可以省去重复创建 url 的步骤。
    const url = file.url || URL.createObjectURL(file.raw || file)
    const video = document.createElement('video')
    video.src = url
    video.onloadedmetadata = () => {
      resolve({ duration: video.duration, width: video.videoWidth, height: video.videoHeight})
      !file.url && URL.revokeObjectURL(url) // 释放URL对象
    }

    video.load() // 开始加载视频文件
  })
}

限制最大执行队列的数量

上述两个方法在优化完成之后,效果并不显著,经过排查,可能是因为 on-change 事件在同一时间触发次数太多导致的。所以写了一套逻辑,限制了一下最大同时执行的队列。

js 复制代码
// 定义数据
const state = reactive({
  currentList: [], // 正在执行的队列
  maxQueue: 5, // 最大执行队列数量
  waitList:[], // 待执行队列
})

// 响应 element-plus upload 组件的 on-change 事件
function changeHandler (file){
  if(state.currentList.length < state.maxQueue){
    handleOne(file)
  } else {
    state.waitList.push(file)
  }
}

// 单个文件处理
async function handleOne(file){
  state.currentList.push(file)
  const { duration, width, height } = await getVideoInfo(file)
  file.duration = duration
  file.dimension = width + 'x' + height
  file.md5Hash = await getFileMd5(file.raw)
  afterHandler(file)
}

// 执行完成之后,判断是否还有待处理的文件,继续执行
function afterHandler (file){
  state.currentList = state.currentList.filter((fl) => {
    return fl.md5Hash !== file.md5Hash && fl.name !== file.name
  })
  if(state.waitList.length){
    let frist = state.waitList[0]
    state.waitList = state.waitList.filter(fl => fl != frist)
    handleOne(frist)
  }
}

然后根据实际情况,调整 state.maxQueue 的值,来控制最大执行队列数量。

总结

经过以上的优化,可以同时选择的文件数量有了大幅提升,目前测试下来,数量提升到700时,页面也还是比较流畅的,再往上就没有进行更进一步的测试。

所以,对于文件总量较多的场景,还是需要通过分批、分片的形式进行处理,减少并发数量,这样的话,可以有效避免出现浏览器性能问题。

相关推荐
outstanding木槿4 分钟前
现实生活例子[特殊字符] 通俗易懂的解释[特殊字符] JS中的原型和原型链[特殊字符]
前端·javascript·js
普宁彭于晏11 分钟前
CSS3相关知识点
前端·css·笔记·学习·css3
天涯学馆18 分钟前
从 REST 到 GraphQL:重塑 API 设计的未来
前端·javascript·面试
blues_C31 分钟前
十三、【核心功能篇】测试计划管理:组织和编排测试用例
vue.js·django·测试用例·drf·测试平台
谢尔登34 分钟前
【React】useId
前端·javascript·react.js
qq_3364117536 分钟前
【笔记】Chrome浏览器的性能调试(performance)工具使用和代码优化
前端·chrome·笔记
笑口常开的小丸子1 小时前
Selenium 中 JavaScript 点击的优势及使用场景
javascript·selenium·测试工具
NoneCoder1 小时前
Redux 实践与中间件应用
前端·react.js·中间件·面试
淡水猫.1 小时前
Next.js 中间件鉴权绕过漏洞 CVE-2025-29927
javascript·安全·web安全·中间件