字玩FontPlayer开发笔记3 性能优化 大量canvas渲染卡顿问题

字玩FontPlayer开发笔记3 性能优化 大量canvas渲染卡顿问题

字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:

github: https://github.com/HiToysMaker/fontplayer

gitee: https://gitee.com/toysmaker/fontplayer

笔记

在笔者的开源项目中,经常需要对大量canvas进行渲染,比如渲染字体预览列表,一个字库可能要包含上千上万个字符,这时候界面会卡住一段时间,体验非常不好。目前项目中主要采用了三个方法进行优化:

  • 加进度条,但是在大量canvas渲染时,界面会彻底卡主,导致进度条不会更新,需要后面两个方法进行辅助
  • 使用requestAnimationFrame将每个canvas的渲染程序间隔一帧再调用,可以有效缓解界面卡顿问题,使进度条流畅运行
  • 使用WebWorker将复杂逻辑另起线程执行,有效防止界面阻塞
1. 进度条

笔者项目使用ElementUI,使用v-loading属性可以为页面添加loading效果,并可以自定义设置loading图标。

但是ElementUI自带的loading效果不包含进度条,所以需要手动加上进度条功能。

进度条代码:

复制代码
<div v-show="loading && total != 0" class="loading-text">
  <el-progress :text-inside="true" :stroke-width="20" :percentage="Math.round(loaded / total * 100)" />
  <div>{{ `加载中,请稍候......已加载${Math.round(loaded / total * 100)}%` }}</div>
</div>

其中使用三个控制变量,loading代表是否为loading状态,total代表总共需要渲染的canvas数量,loaded表示已经渲染的canvas数量。

效果如下:

2. requestAnimationFrame

requestAnimationFrame的作用是在下一帧运行前时回调运行指定函数,也就是每个回调函数会相隔一帧的时间再运行。

大量canvas连续渲染会阻塞界面渲染进程,使用requestAnimationFrame将canvas渲染进行时间上的间隔可以有效防止界面阻塞。

关键代码:

ts 复制代码
// 渲染函数
const render = () => {
  // i 超过 length,渲染完毕
  if (i >= characters.length) return
  // 渲染第i个字符
  const characterFile = characters[i]
  if (!characterFile._o) {
    // 执行字符脚本
    executeCharacterScript(characterFile)
  }
  // 获取字符预览canvas
  const canvas: HTMLCanvasElement = document.getElementById(`preview-canvas-${characterFile.uuid}`) as HTMLCanvasElement
  if (!canvas) return
  // 将字符数据处理成预览模式
  const contours: Array<Array<ILine | IQuadraticBezierCurve | ICubicBezierCurve>> = componentsToContours(orderedListWithItemsForCharacterFile(characterFile), {
    unitsPerEm,
    descender,
    advanceWidth: unitsPerEm,
  }, { x: 0, y: 0 }, false, true)
  // 渲染字符
  renderPreview2(canvas, contours)
  // 更新进度条
  if (loading.value) {
    loaded.value += 1
    if (loaded.value >= total.value) {
      loading.value = false
    }
  }
  // i递增
  i++
  // 如果没有渲染完毕,调用requestAnimationFrame对下一个字符渲染进行回调
  if (i < characters.length) {
    requestAnimationFrame(render)
  }
}
// 调用requestAnimationFrame渲染第一个字符
requestAnimationFrame(render)
3. WebWorker

WebWorker可以将逻辑从主线程中分离出来,在一个新的线程中运行,从而避免界面阻塞。

对于一些复杂操作,使用WebWorker可以有效提高性能。

笔者项目中,导入字体库的处理过程放在了worker线程中执行,关键代码如下:

worker/index.ts中定义初始化函数

ts 复制代码
const initWorker = () => {
  let worker = null
  if (window.Worker) {
	worker = new Worker(new URL('./worker.ts', import.meta.url))
  }
  return worker
}

main.ts中调用worker初始化函数

ts 复制代码
const worker = initWorker()

主线程需要调用worker线程时,对worker线程发出消息,消息中包含所要处理的数据

ts 复制代码
worker.postMessage([WorkerEventType.ParseFont, font, selectedFile.value.width])

在worker/worker.ts中处理从主线程中收到的消息

ts 复制代码
onmessage = (e) => {
  switch(e.data[0]) {
	case WorkerEventType.ParseFont: {
	  const font = e.data[1]
	  const width = e.data[2]
	  // ...
	  // 此处省略逻辑代码
	  // ...
	
      // 将处理好的数据传递给主线程
	  postMessage(list)
      break
	}
  }
}

主线程收到消息后,进行后续逻辑

ts 复制代码
worker.onmessage = (e) => {
  const list = e.data
  selectedFile.value.characterList = list
  clearCharacterRenderList()
  characterList.value.map((characterFile) => {
    addCharacterTemplate(generateCharacterTemplate(characterFile))
  })
  loading.value = false
  emitter.emit('renderPreviewCanvas', true)
}
相关推荐
雪碧聊技术7 分钟前
前端VUE3项目部署到linux服务器(CentOS 7)
前端·linux部署vue3项目
汝生淮南吾在北3 小时前
SpringBoot+Vue饭店点餐管理系统
java·vue.js·spring boot·毕业设计·毕设
酒尘&6 小时前
JS数组不止Array!索引集合类全面解析
开发语言·前端·javascript·学习·js
学历真的很重要6 小时前
VsCode+Roo Code+Gemini 2.5 Pro+Gemini Balance AI辅助编程环境搭建(理论上通过多个Api Key负载均衡达到无限免费Gemini 2.5 Pro)
前端·人工智能·vscode·后端·语言模型·负载均衡·ai编程
思成不止于此7 小时前
MySQL 查询实战(三):排序与综合练习
数据库·笔记·学习·mysql
深海潜水员7 小时前
OpenGL 学习笔记 第一章:绘制一个窗口
c++·笔记·学习·图形渲染·opengl
用户47949283569158 小时前
"讲讲原型链" —— 面试官最爱问的 JavaScript 基础
前端·javascript·面试
用户47949283569158 小时前
2025 年 TC39 都在忙什么?Import Bytes、Iterator Chunking 来了
前端·javascript·面试
JIngJaneIL8 小时前
基于Java非遗传承文化管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
+VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue心理健康管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计