我不允许你们学会了worker却还没有应用场景

14,280 阅读10分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本文相关demo

本文相关仓库

前言

如果生活存在奇迹,那一定是努力的轨迹!

hello,小伙伴们好呀,我是小羽同学~

距上次发布的《项目没亮点?那就来学下web worker吧~》,有很多的小伙伴都说学会了,但是在实际的项目中却好像没有什么实战的场景,毫无用武之地呀。

emmm,如果没有深入的去思考项目的可优化点的时候,的确会有这种想法。

但是问题不大,小羽将在这篇文章中举两个简单易懂,并且是可以有机会应用到大家项目中的例子!!!

尤其是做后台管理系统的同学,不要怕你们的项目没有亮点,你们也可以大展身手啦~

查漏补缺

上期的《项目没亮点?那就来学下web worker吧~》一文中说漏了一些相关的知识点,然后在评论区里面也有小伙伴们讨论了,小羽自己也经过一番查漏补缺后,在这里先简单的做一个总结,如果有错误的地方小伙伴们可以及时指出。

浏览器中会存在如下这些进程

  • 浏览器主进程:负责协调、主控、只有一个
  • GPU进程:用于3D绘制,可以禁用,只有一个
  • 第三方进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • 渲染进程:浏览器渲染进程(render进程),即通常说的浏览器内核,主要作用是:页面渲染、脚本执行、事件处理等。每一个标签页的打开都会创建一个Render进程,并且互不影响。默认的话一个标签页对应一个Render进程,但是,有时候浏览器会将多个进程合并,如打开了多个空白标签页。

此外还会存在很多线程,如:

  • GUI渲染线程:主要负责渲染浏览器界面,解析HTML,CSS,构建DOM树和Render树,布局和绘制、以及回流重绘等。
  • JS引擎线:JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码。
  • 事件触发线程:事件触发线程归属于浏览器,而不是属于JS引擎,JS引擎处理的事务过多,需要浏览器另开线程来进行协助
  • 定时器触发线程:即setInterval与setTimeout所在线程
  • 异步http请求线程:XMLHttpRequest连接后通过浏览器新开一个线程请求
  • worker线程

其中GUI渲染线程和JS引擎线程是互斥的,他们会共用渲染进程,当JS引擎执行时,GUI线程就会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时,才会被执行。

举一个简单的小例子,比如咱们通过requestAnimationFrame这个api每16.6ms获取一次时间,并且渲染。当JS引擎线程繁忙的时候,GUI线程就会被挂起,即不会更新咱们页面上的时间,从而导致卡顿的现象。

Excel大文件导出

如果小伙伴们做的项目是普普通通的后台管理系统,那么我强烈推荐你看一下这一小节。

因为做后台系统的同学肯定有做过table表格相关的需求。而当一个后台系统存在table表格时,那么它很大概率还会伴随着导出表格的功能。

所以我不相信没有,没有的同学骑上你们的小电驴去,提起40米的大刀,去找你们的产品经理吧。

img

首先感谢这位同学,让我想到了web worker在后台管理系统中存在这么一个通用的解决方案

image-20220927003235447

那咱们先简单说一下主线程导出worker线程导出的逻辑吧,为了减少网络等因素的影响,咱们将所有的表格数据在初始化前就构造好了假数据,仅处理excel导出时逻辑

  • 主线程导出:通过exceljs构建表格相关的参数,传入相关的数据,然后转换为blob流,最后通过file-save导出
  • worker线程导出:通过ExcelWokrer创建worker线程,通过postmessage向worker线程传递相应的excel数据,在worker线程中通过exceljs构建表格相关数据,然后转换为blob流,接着将生成的blob流通过postmessage传回来主线程,最后通过file-save导出
  • 时间戳渲染:时间戳渲染主要是用来测试咱们的JS引擎线程是否繁忙,从而导致GUI线程被挂起,从而导致卡顿的现象。
// index.tsx
import { Button, Table } from 'antd';
import React, { useState, useEffect } from 'react';
import ExcelJS from 'exceljs';
import FileSaver from 'file-saver';
import ExcelWorker from './excel.worker?worker';
​
const dataSource = [];
​
for (let i = 1; i < 30000; i++) {
  dataSource.push({
    key: i,
    name: `name-${i}`,
    age: 18,
    tag: '小羽同学',
    value1: `value1-${i}`,
    value2: `value2-${i}`,
    value3: `value3-${i}`,
  });
}
​
const columns = [
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '年龄',
    dataIndex: 'age',
    key: 'age',
  },
  {
    title: '标签',
    dataIndex: 'tag',
    key: 'tag',
  },
  {
    title: 'value1',
    dataIndex: 'value1',
    key: 'value1',
  },
  {
    title: 'value2',
    dataIndex: 'value2',
    key: 'value2',
  },
  {
    title: 'value3',
    dataIndex: 'value3',
    key: 'value3',
  },
];
​
export default function Excel() {
  const [showTime, setShowTime] = useState(Date.now());
​
  useEffect(() => {
    updateShowTime();
  }, []);
​
  const updateShowTime = () => {
    setShowTime(Date.now());
    requestAnimationFrame(updateShowTime);
  };
  // 主线程导出Excel
  const mainExportExcel = () => {
    // 创建工作簿
    const workbook = new ExcelJS.Workbook();
    // 添加工作表
    const worksheet = workbook.addWorksheet('sheet1');
​
    // 设置表格内容
    const _titleCell = worksheet.getCell('A1');
    _titleCell.value = 'Hello ExcelJS!';
​
    const workBookColumns = columns.map((item) => ({
      header: item.title,
      key: item.key,
      width: 32,
    }));
    worksheet.columns = workBookColumns;
    worksheet.addRows(dataSource);
​
    // 导出表格
    workbook.xlsx.writeBuffer().then((buffer) => {
      let _file = new Blob([buffer], {
        type: 'application/octet-stream',
      });
      FileSaver.saveAs(_file, 'ExcelJS.xlsx');
    });
  };
​
  // 子线程导出Excel
  const workerExportExcel = async () => {
    const _file = await new Promise((resolve, reject) => {
      const myWorker = new ExcelWorker();
      myWorker.postMessage({
        columns,
        dataSource,
      });
      myWorker.onmessage = (e) => {
        resolve(e.data.data); // 关闭worker线程
        myWorker.terminate();
      };
    });
    FileSaver.saveAs(_file, 'ExcelJS.xlsx');
  };
​
  return (
    <div>
      <Button onClick={mainExportExcel}>导出全部</Button>
      <Button onClick={workerExportExcel}>worker导出全部</Button>
      <span>{showTime}</span>
      <Table dataSource={dataSource} columns={columns} />
    </div>
  );
}
​
// excel.worker.ts
import ExcelJS from 'exceljs';
​
// onmessage事件
onmessage = function (e) {
  const {
    data: { columns, dataSource },
  } = e;
  // 创建工作簿
  const workbook = new ExcelJS.Workbook();
  // 添加工作表
  const worksheet = workbook.addWorksheet('sheet1');
​
  // 设置表格内容
  const _titleCell = worksheet.getCell('A1');
  _titleCell.value = 'Hello ExcelJS!';
​
  const workBookColumns = columns.map((item) => ({
    header: item.title,
    key: item.key,
    width: 32,
  }));
  worksheet.columns = workBookColumns;
  worksheet.addRows(dataSource);
​
  // 导出表格
  workbook.xlsx.writeBuffer().then((buffer) => {
    let _file = new Blob([buffer], {
      type: 'application/octet-stream',
    });
    // 将获取的数据通过postMessage发送到主线程
    self.postMessage({
      data: _file,
      name: 'worker test',
    });
    self.close();
  });
};

主线程导出3w数据量的excel表格如下图,从中可以明显的发现咱们的时间戳渲染有明显的卡顿现象

main_excel

worker线程导出3w数据量的excel表格如下图,从中可以发现咱们的时间戳渲染并无明显卡顿现象,还是照常渲染。

worker_excel

离屏canvas

image.png

在说下一个例子之前,咱们先补充一个新的知识点。

离屏canvas(OffscreenCanvas) 是一个实验中的新特性,主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验。

OffscreenCanvascanvas都是渲染图形的对象。 不同的是canvas只能在window环境下使用,而OffscreenCanvas即可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染成为可能。

它的兼容性如下图,可以发现兼容了最新的chromeedge以及firefox,但不兼容safiriie浏览器,所以项目需要兼容这两种浏览器的小伙伴们可以溜了~

image-20220928084125868

图片批量压缩

小伙伴们在项目中应该都会有做过图片上传的需求吧?

但是如果我们直接传原图的话,会很大,不仅占用用户的流量,而且还会占用oss/cdn/服务器容量,这样子对用户和公司来说都是不负责的行为吧,所以在上传图片之前,咱们通常都会对图片进行压缩。而图片压缩,在前端通常则是使用canvas

单个图片直接在主线程进行压缩没什么问题,但是如果需要上传100张图片,并且在上传之前需要对这100张图片进行压缩的话。此时,对咱们的主线程就会有比较大的压力了。因此,在数量较大的情况下,建议尝试使用woker线程对图片进行压缩,从而降低咱们主线程的压力。

对了,这里咱们需要注意的是,在worker中是没有dom的,所以咱们就无法通过domcument.createElement('canvas')来创建canvas,需要用到的是离屏canvas。但离屏canvas中是没有toDataUrl对象的,需要先转blob后,才可以转base64(当然,上传文件咱们一般都是传blob)

  • 主线程压缩:加载图片,创建canvas,在canvas中绘制图片,通过toDataUrl压缩并生成base64
  • worker线程压缩:通过fetch加载图片并转换为blob流,创建多个worker线程,然后将图片的blob流组成数组后通过postmessage传给worker线程,然后创建离屏canvas,在离屏canvas中绘制图片,通过convertToBlob将图片压缩并生成blob流,接着通过FileReader将blob流转换为图片后,最后通过postmessage传递回来主线程
  • 时间戳渲染:时间戳渲染主要是用来测试咱们的JS引擎线程是否繁忙,从而导致GUI线程被挂起,从而导致卡顿的现象。
// index.tsx
import { Button, Table } from 'antd';
import React, { useState, useEffect } from 'react';
import ComporessWorker from './compress.worker?worker';
​
const allImgNum = 100;
const url =
  'https://pic3.zhimg.com/v2-58d652598269710fa67ec8d1c88d8f03_r.jpg?source=1940ef5c';
​
export default function Home() {
  const [showTime, setShowTime] = useState(Date.now());
​
  useEffect(() => {
    updateShowTime();
  }, []);
​
  const updateShowTime = () => {
    setShowTime(Date.now());
    requestAnimationFrame(updateShowTime);
  };
​
  // 主线程压缩图片
  const mainCompressImg = async () => {
    const res = await new Promise((resolve, reject) => {
      const img = new Image();
      img.src = url;
      img.setAttribute('crossOrigin', 'Anonymous');
      img.onload = () => {
        resolve(img);
      };
      img.onerror = (e) => {
        reject(e);
      };
    });
    // console.log(res);
    console.time('compress');
    for (let i = 0; i < allImgNum; i++) {
      const compressRes = compressImg(res);
      // console.log(compressRes);
    }
    console.timeEnd('compress');
    return res;
  };
​
  const compressImg = (img) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    return canvas.toDataURL('image/jpeg', 0.75);
  };
​
  // 子线程压缩图片
  const workerCompressImg = async () => {
    const res = await fetch(url).then((res) => res.blob());
    const workerList = [[], [], [], [], []];
    for (let i = 0; i < allImgNum / 5; i++) {
      workerList[0].push(res);
      workerList[1].push(res);
      workerList[2].push(res);
      workerList[3].push(res);
      workerList[4].push(res);
    }
​
    console.time('compressWorker');
    const pList = [];
    for (let item of workerList) {
      const compressP = new Promise((resolve, reject) => {
        const myWorker = new ComporessWorker();
        myWorker.postMessage({
          imageList: item,
        });
        myWorker.onmessage = (e) => {
          resolve(e.data.data);
        };
      });
      pList.push(compressP);
    }
​
    const pRes = await Promise.all(pList);
    console.log(pRes);
    console.timeEnd('compressWorker');
  };
​
  return (
    <div>
      <Button onClick={mainCompressImg}>压缩图片</Button>
      <Button onClick={workerCompressImg}>worker压缩图片</Button>
      <span>{showTime}</span>
    </div>
  );
}
// compress.worker.ts
​
// onmessage事件
onmessage = async function (e) {
  const {
    data: { imageList },
  } = e;
​
  const resList = [];
​
  for (let img of imageList) {
    // @ts-ignore
    const offscreen = new OffscreenCanvas(100, 100);
    const ctx = offscreen.getContext('2d');
    const imgData = await createImageBitmap(img);
    offscreen.width = imgData.width;
    offscreen.height = imgData.height;
    ctx.drawImage(imgData, 0, 0, offscreen.width, offscreen.height);
    const res = await offscreen
      .convertToBlob({ type: 'image/jpeg', quality: 0.75 })
      .then((blob) => {
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        return new Promise((resolve) => {
          reader.onloadend = () => {
            resolve(reader.result);
          };
        });
      });
    resList.push(res);
  }
​
  self.postMessage({
    data: resList,
    name: 'worker test',
  });
  self.close();
};

主线程对同一张图片压缩图片100次的结果如下图,可以发现主线程在压缩时压力较大,将GUI渲染线程挂起,导致页面卡顿的现象发生,并且整体的图片压缩时间为108s

main_compress

创建5个worker线程,并对同一张图片压缩100次(每个worker线程压缩20次),可以发现此时咱们的GUI线程还是在继续渲染,未被js脚本阻塞,并且整体的图片压缩时间也下降到了37s

worker_compress

小结

本文主要是针对小伙伴们提出的:很难在日常的开发中使用到worker线程的问题。 介绍了两个可以应用在日常开发中的场景——excel大文件导出图片批量压缩。希望可以帮助到一些感觉自己项目缺少亮点的同学,以及遇到类似问题时,可以有多一种解决的思路

此外,还对上篇文章中的缺失的浏览器进程和线程部分内容进行了一些补充,以及介绍了离屏canvas~

如果看这篇文章后,感觉有收获的小伙伴们可以点赞+收藏哦~

img

如果想和小羽交流技术可以加下wx,也欢迎小伙伴们来和小羽唠唠嗑,嘿嘿~