记录用前端代替后端生成zip的过程,速度快了 57 倍!!!

业务场景:

产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。

管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)至少需要 10s。有什么方法能够优化下。

因为代码不具备可复用性,因此部分代码直接省略,思路为主

原始逻辑

js 复制代码
  public async getZip(themeId: string, res: any) {
    const theme = await this.model.findById(themeId); // 从数据库

    // 这里需要借用一个服务器上的主题模板文件夹 template/,
    
    /*
       theme = {
              wallpapers: [
                  { url: 'https://亚马逊云.com/1.jpg', ... },
                  ...
              ]
       }
    */
    
    // for 循环遍历 theme.wallpapers , 并通过 fetch 请求 url,将其写进 template/static/wallpapers 文件夹中
    theme.wallpapers.map((item) => {
        const response = await fetch(item.url);
        const buffer = new Uint8Array(await response.arrayBuffer());
        await fs.writeFile(`template/wallpapers/${fileName}`, buffer);
    })
    
    // ... 还有其他一些处理
    
    // 将 template 压缩成 zip 文件,发送给前端
  }

思考 ing ...

1 利用图片可以被浏览器缓存

当一次下载主题从请求亚马逊云的图片数据,这步没有问题。 但是当重复下载的时候,之前下载过的图片又会再次下载,操作人员每次都需要等个十几秒,这就不太友好了。这部分时间花费还是挺多的。

可以利用下浏览器能够将图片缓存到 disk cache 中的特点,将这部分的代码逻辑放到前端完成,因为还需要对压缩包中的文件做一些处理,因此需要借助下 jszip 这个库。

看下改后的代码

js 复制代码
onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme() 
    const template = api.getTemplate() // Blob
    
    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据
    
    console.time('handle images')
    const wallpaperList = theme.wallpapers
    for (const wallpaper of wallpaperList) {
      const response = await fetch(wallpaper.url) // 请求图片数据
      const buffer = new Uint8Array(await response.arrayBuffer())
      const fileName = wallpaper.url.split('/').pop()
      zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) // 写进压缩包
    }
    console.timeEnd('handle images') // 统计用时
    
    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...
    
    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
      (base64) => {
        const link = document.createElement('a')
        link.href = 'data:application/zip;base64,' + base64
        link.download = 'template.zip'
        link.target = '_blank'
        link.style.display = 'none'

        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
      },
      (err) => {
        console.log('打包失败', err)
      }
    )
}

优化完成

当第一次下载时,handle images 步骤耗时 20 - 21 s,流程和后端差不多。

当第二次下载时,handle images 步骤耗时 0.35s - 0.45 s。会直接读取 disk cache 中的图片数据,50 ms 内就完成了。

速度快了 57 倍有余!!! , 你还能想到其他优化方式吗?继续往后看 🍒

第一次请求各个图片耗时

第二次请求各个图片耗时

2 并发请求

我们都知道,浏览器会为每个域名维持 6 个 TCP 链接 (再拓展还有域名分片知识),我们是否可以利用这个特点做些什么?

答案是:并发上传

通过上面的代码,可以看到,每个图片请求都是串行的,一个图片请求完了再进行下一个图片请求。我们一次请求 4 个图片,这样就更快了。

首先写一个能够管理并发任务的类

ts 复制代码
export class TaskQueue {
  public queue: {
    task: <T>() => Promise<T>
    resolve: (value: unknown) => void
    reject: (reason?: any) => void
  }[]
  public runningCount: number  // 正在执行的任务数量
  public tasksResloved?: (value: unknown) => void
  public tasksRejected?: (reason?: any) => void

  public constructor(public maxConcurrency: number = 4) { // 最多同时执行 4 个任务
    this.queue = [] // 任务队列
    this.runningCount = 0
  }
  
  // 添加任务
  public addTask(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject })
    })
  }
    
  // 执行
  public run() {
    return new Promise((resoved, rejected) => {
      this.tasksResloved = resoved
      this.tasksRejected = rejected
      this.nextTask()
    })
  }

  private nextTask() {
    if (this.queue.length === 0 && this.runningCount === 0) {
      this.tasksResloved?.('done')
      return
    }
    
    // 如果任务队列中还有任务, 并且没有到最大执行任务数,就继续取出任务执行
    while (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
      const { task, resolve, reject } = this.queue.shift()
      this.runningCount++
      task()
        .then((res) => {
          this.runningCount--
          resolve(res)
          this.nextTask()
        })
        .catch((e) => {
          this.runningCount--
          reject(e)
          this.nextTask()
        })
    }
  }
}

改造代码

js 复制代码
onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme() 
    const template = api.getTemplate() // Blob
    
    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据
    
    console.time('handle images')
    const wallpaperList = theme.wallpapers
    
    // 注释之前的逻辑
    // for (const wallpaper of wallpaperList) {
    //   const response = await fetch(wallpaper.url) 
    //   const buffer = new Uint8Array(await response.arrayBuffer())
    //   const fileName = wallpaper.url.split('/').pop()
    //   zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) 
    // }
    
    const taskQueue = new TaskQueue() // 新建任务队列,默认同时执行 4 个
    for (const wallpaper of wallpaperList) {
      taskQueue
        .addTask(() => fetch(wallpaper.url)) // 添加任务
        .then(async (res) => {  // 任务执行完后的回调
          const buffer = new Uint8Array(await (res as Response).arrayBuffer())
          const fileName = wallpaper.url.split('/').pop()
          zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
        })
        .catch((e) => console.log('壁纸获取失败', e))
    }
    await taskQueue.run()  // 等待所有图片都拿到
    console.timeEnd('handle images') // 统计用时
    
    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...
    
    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
      (base64) => {
        const link = document.createElement('a')
        link.href = 'data:application/zip;base64,' + base64
        link.download = 'template.zip'
        link.target = '_blank'
        link.style.display = 'none'

        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
      },
      (err) => {
        console.log('打包失败', err)
      }
    )
}

大功告成!

当第一次下载时,handle images 步骤耗时 7 s,速度是之前的 3 倍。

当第二次下载时,handle images 步骤耗时 0.25s,速度是之前的 1.4 - 1.8

3 更多的可能

越来越感觉到计算机网络的重要性, 还有未实现的优化方式:

  1. 域名分片,更多的并发(也有劣势 ,比如 每个域都需要额外的 DNS 查找成本以及建立每个 TCP 连接的开销, TCP 慢启动带宽利用不足)
  2. 升级 HTTP2 这不是靠前端一人能够完成的

如果学到了新知识,麻烦点个 👍 和 ⭐

相关推荐
passerby606142 分钟前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte3 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc