普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月26日首页

高性能直播弹幕系统实现:从 Canvas 2D 到 WebGPU

作者 Jydud
2026年2月26日 11:22

高性能直播弹幕系统实现:从 Canvas 2D 到 WebGPU

前言

在现代直播应用中,弹幕是提升用户互动体验的重要功能。本文将深入介绍如何实现一个支持大规模并发、高性能渲染的弹幕系统,该系统支持 Canvas 2DWebGPU 两种渲染方式,能够在不同设备环境下自适应选择最佳渲染方案。

技术选型与架构设计

整体架构

我们的弹幕系统采用了以下架构设计:

┌─────────────────────┐
│  DanmakuCanvas.vue  │  ← Vue组件层(UI交互)
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│  DanmakuManager.ts  │  ← 管理层(协调通信)
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│     worker.js       │  ← Worker层(核心渲染逻辑)
└─────────────────────┘

核心特性:

  • 🚀 使用 Web Worker 实现离屏渲染,避免阻塞主线程
  • 🎨 支持 Canvas 2D 和 WebGPU 双渲染引擎
  • 📊 智能轨道分配算法,防止弹幕碰撞
  • 🎯 支持富文本渲染(文字 + 表情)
  • 📈 性能监控与数据上报
  • 🔄 响应式画布尺寸适配

技术栈

  • Vue 3: 组件层框架
  • TypeScript: 类型安全
  • OffscreenCanvas: 离屏渲染
  • Web Worker: 多线程
  • WebGPU: GPU加速渲染(可选)

核心实现详解

一、Vue 组件层实现

DanmakuCanvas.vue 作为用户界面层,主要负责:

<template>
  <div class="xhs-danmaku-container">
    <canvas ref="canvasRef" class="xhs-danmaku-container-canvas" />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import DanmakuManager from './danmakuManager'

const props = defineProps({
  position: { type: String, default: 'top' },
  emojis: null,
  showDanmaku: { type: Boolean, default: true },
  config: { type: Object, default: null },
})

const canvasRef = ref<HTMLCanvasElement>()
const danmakuManager = ref<DanmakuManager>()

// 初始化弹幕管理器
function init() {
  if (!canvasRef.value) return
  
  danmakuManager.value = new DanmakuManager(
    handleError, 
    handleErrorReport, 
    updateHeartDim, 
    logger
  )
  danmakuManager.value.init(canvasRef.value, props.emojis, props.config)
}

// 添加弹幕的公共方法
function addDanmaku(message: string, options: any = { type: 'scroll' }) {
  if (!danmakuManager.value || !message.trim()) return
  danmakuManager.value?.addDanmaku(message, options)
}

// 响应式尺寸适配
function updateCanvasSize() {
  if (!danmakuManager.value || !canvasRef.value) return
  
  const rect = canvasRef.value.getBoundingClientRect()
  const newConfig = { 
    canvasWidth: rect.width, 
    canvasHeight: rect.height 
  }
  danmakuManager.value.updateConfig(newConfig)
}

onMounted(() => {
  init()
  if (props.showDanmaku) {
    danmakuManager.value?.start()
  }
  
  // 监听窗口变化
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleResize)
})

onUnmounted(() => {
  danmakuManager.value?.destroy()
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleResize)
})

// 暴露方法给父组件
defineExpose({
  addDanmaku,
  openDanmaku,
  closeDanmaku,
  playDanmaku,
  pauseDanmaku,
})
</script>

关键点:

  1. 使用 ref 获取 canvas DOM 元素
  2. 生命周期管理:初始化 → 运行 → 销毁
  3. 监听窗口 resize 和全屏事件,实时调整画布尺寸
  4. 通过 defineExpose 暴露控制接口

二、管理层实现

danmakuManager.ts 负责主线程与 Worker 线程的通信:

export default class DanmakuManager {
  private worker: Worker | null = null
  private onError: (err: any) => void
  private onErrorReport: (data: any) => void
  private updateHeartDim: (key: string, value: any) => void

  constructor(
    onError: (err: any) => void,
    onErrorReport: (data: any) => void,
    updateHeartDim: (key: string, value: any) => void,
    logger?: any,
  ) {
    this.onError = onError
    this.onErrorReport = onErrorReport
    this.updateHeartDim = updateHeartDim
    
    try {
      // 创建 Web Worker
      this.worker = work(require.resolve('./worker.js'))
      this.worker.onerror = this.handleError.bind(this)
      this.worker.onmessage = this.handleMessage
    } catch (error) {
      this.logger.warn('创建弹幕 Worker 失败:', error)
      this.onError(error)
    }
  }

  // 初始化离屏Canvas
  init = (canvas: HTMLCanvasElement, mojiData: any, config: any) => {
    try {
      // 转移 Canvas 控制权到 Worker
      const offScreenCanvas = canvas.transferControlToOffscreen()
      
      const emojis = this.serializeMojiData(mojiData)
      const rect = canvas.getBoundingClientRect()
      
      // 向 Worker 发送初始化消息
      this.worker?.postMessage({
        type: 'INIT',
        data: {
          config: {
            canvasWidth: rect.width,
            canvasHeight: rect.height,
            pixelRatio: window.devicePixelRatio || 1,
            emojis,
            ...config,
          },
          danmuRenderType: localStorage.getItem('danmuRenderType'),
          offScreenCanvas,
        },
      }, [offScreenCanvas]) // 转移对象所有权
    } catch (error) {
      this.onError(error)
    }
  }

  // 添加弹幕
  addDanmaku(message: string, options: any) {
    this.worker?.postMessage({ 
      type: 'ADD_DANMAKU', 
      data: { message, options } 
    })
  }

  // 更新弹幕配置(用于响应式调整)
  updateConfig(newConfig: any) {
    this.worker?.postMessage({ 
      type: 'UPDATE_CONFIG', 
      data: { newConfig } 
    })
  }

  // 销毁 Worker
  destroy() {
    this.worker?.terminate()
  }
}

核心技术点:

  1. OffscreenCanvas 转移:通过 transferControlToOffscreen() 将 Canvas 控制权转移到 Worker 线程
  2. 结构化克隆:使用 postMessage 的第二个参数传递可转移对象
  3. ImageBitmap 序列化:将表情图片转换为可传输的 ImageBitmap 对象

三、Worker 核心渲染逻辑

worker.js 是整个系统的核心,包含以下关键模块:

3.1 弹幕数据结构
class Danmaku {
  constructor(message, options, config, ctx) {
    const type = options?.type || 'scroll'
    const parts = this.parseRichText(message)
    const width = this.computeDanmakuWidth(parts, options, config, ctx)
    const boxWidth = options.showBorder ? width + PADDING_LEFT * 2 : width
    const speed = options.speed || config.speed

    this.id = this.getDanmakuId()
    this.text = message
    this.type = type
    this.speed = type === 'scroll' ? speed : 0
    this.parts = parts           // 富文本片段
    this.width = width
    this.boxWidth = boxWidth
    this.x = this.getDanmakuX(boxWidth, type, config)
    this.timestamp = Date.now()
    this.color = options.color || config.color
    this.fontSize = options.fontSize || config.fontSize
    this.priority = options.priority || 0
    this.showBorder = options.showBorder || false
  }

  // 解析富文本(文字+表情)
  parseRichText(message) {
    const parts = []
    let lastIndex = 0
    const matches = [...message.matchAll(/\[([^\]]+)\]/g)]
    
    if (matches.length === 0) return []
    
    for (const match of matches) {
      // 添加普通文本
      if (match.index > lastIndex) {
        parts.push({
          type: 'text',
          content: message.slice(lastIndex, match.index),
        })
      }
      // 添加表情
      parts.push({
        type: 'emoji',
        content: match[0],
      })
      lastIndex = match.index + match[0].length
    }
    
    // 添加剩余文本
    if (lastIndex < message.length) {
      parts.push({
        type: 'text',
        content: message.slice(lastIndex),
      })
    }
    return parts
  }
}

设计亮点:

  • 富文本解析:支持 [表情名] 格式的表情符号
  • 动态宽度计算:精确计算文字+表情的混合宽度
  • 优先级系统:支持 VIP 弹幕等优先展示场景
3.2 渲染器实现
class DanmakuRenderer {
  constructor(config, ctx) {
    this.config = config
    this.ctx = ctx
  }

  render(danmakuList) {
    danmakuList.forEach((danmaku) => {
      if (danmaku.showBorder) {
        this.drawDanmakuWithBorder(danmaku)
      } else {
        this.renderRichDanmaku(danmaku)
      }
    })
  }

  // 富文本弹幕渲染
  renderRichDanmaku(danmaku) {
    this.setupCanvasContext(danmaku)
    
    const startX = danmaku.x
    const yPosition = danmaku.y
    
    if (!danmaku.parts || danmaku.parts.length === 0) {
      this.renderSimpleDanmaku(danmaku.text, startX, yPosition)
      return
    }
    
    this.renderParts(danmaku, startX, yPosition)
  }

  // 渲染富文本各部分
  renderParts(danmaku, startX, yPosition) {
    let currentX = startX
    
    for (const part of danmaku.parts) {
      const { content, type } = part || {}
      if (!content) continue

      if (type === 'emoji') {
        currentX = this.renderEmoji(content, danmaku, currentX, yPosition)
      } else {
        currentX = this.renderText(content, danmaku, currentX, yPosition)
      }
    }
  }

  // 渲染文本
  renderText(content, danmaku, x, y) {
    this.ctx.strokeText(content, x, y)
    this.ctx.fillText(content, x, y)
    return x + this.measureTextWidth(content, danmaku).width
  }

  // 渲染表情
  renderEmoji(content, danmaku, x, y) {
    try {
      const emojiBitmap = this.config.emojis[content]?.bitmap
      
      if (!emojiBitmap) {
        // 回退到文本渲染
        return this.renderText(content, danmaku, x, y)
      }

      const emojiActualSize = danmaku.fontSize
      const emojiY = y - emojiActualSize / 2

      this.ctx.drawImage(
        emojiBitmap,
        x,
        emojiY,
        emojiActualSize,
        emojiActualSize,
      )

      return x + danmaku.fontSize
    } catch (error) {
      return this.renderText(content, danmaku, x, y)
    }
  }
}

渲染优化:

  1. 文字描边:使用 strokeText + fillText 提升可读性
  2. 混排处理:文字和表情按顺序依次渲染
  3. 容错机制:表情加载失败时回退到文本显示
3.3 智能轨道分配算法
class DanmakuWorker {
  constructor() {
    this.danmakuList = []      // 屏幕上的弹幕
    this.penddingList = []     // 等待队列
    this.usedTrackIds = new Set()  // 已占用轨道
    this.config = defaultConfig
  }

  // 创建轨道列表
  createTrackList() {
    const { trackCount, trackHeight, trackGap } = this.config
    return Array.from({ length: trackCount }, (_, i) => ({
      id: `${i}-track`,
      height: trackHeight * (i + 1) + trackGap / 2,
    }))
  }

  // 为新弹幕分配轨道
  assignTrack(newDanmaku) {
    const trackList = this.config.trackList
    
    // 优先分配未使用的轨道
    if (this.usedTrackIds.size < trackList.length) {
      return trackList.find(track => !this.usedTrackIds.has(track.id))
    }

    // 检查每个轨道是否有足够空间
    for (const track of trackList) {
      if (this.isTrackAvailable(track, newDanmaku)) {
        return track
      }
    }
    
    return null  // 无可用轨道
  }

  // 检查轨道是否可用
  isTrackAvailable(track, newDanmaku) {
    if (newDanmaku.type !== 'scroll') {
      // 固定弹幕:确保轨道上没有其他固定弹幕
      const sameTrackDanmakus = this.danmakuList.filter(
        d => d.type !== 'scroll' && d.trackId === track.id,
      )
      return sameTrackDanmakus.length === 0
    }

    // 滚动弹幕:检查是否有足够空间
    const sameTrackDanmakus = this.danmakuList.filter(
      d => d.type === 'scroll' && d.trackId === track.id,
    )
    
    if (sameTrackDanmakus.length === 0) return true

    // 检查最后一个弹幕是否已留出足够空间
    const lastDanmaku = sameTrackDanmakus[sameTrackDanmakus.length - 1]
    const lastDanmakuPosition = lastDanmaku.x + lastDanmaku.boxWidth
    const availableSpace = this.config.canvasWidth - lastDanmakuPosition
    
    return availableSpace >= SAFE_AREA  // 36px 安全距离
  }
}

算法特点:

  • 空间优先:优先使用完全空闲的轨道
  • 碰撞检测:计算前一条弹幕是否留出足够安全距离
  • 队列机制:无可用轨道时加入等待队列
3.4 WebGPU 渲染实现
async initWebGpu() {
  if (!navigator.gpu) {
    return false
  }

  // 获取 GPU 适配器和设备
  const adapter = await navigator.gpu.requestAdapter()
  const device = await adapter.requestDevice()
  const context = this.offScreenCanvas.getContext('webgpu')

  // 创建辅助 Canvas 用于 2D 绘制
  const webgpuCanvas = new OffscreenCanvas(
    this.offScreenCanvas.width, 
    this.offScreenCanvas.height
  )
  const webgpuCtx = webgpuCanvas.getContext('2d')
  
  this.webgpuCanvas = webgpuCanvas
  this.ctx = webgpuCtx  // 使用 2D 上下文绘制,再由 GPU 渲染

  // 配置 Canvas 格式
  const canvasFormat = navigator.gpu.getPreferredCanvasFormat()
  context.configure({
    device,
    format: canvasFormat,
    alphaMode: 'premultiplied',
  })

  // 创建着色器
  const vertexShaderCode = `
    struct VertexOutput {
      @builtin(position) position: vec4f,
      @location(0) uv: vec2f,
    };

    @vertex
    fn main(@location(0) position: vec2f, @location(1) uv: vec2f) -> VertexOutput {
      var output: VertexOutput;
      output.position = vec4f(position, 0.0, 1.0);
      output.uv = uv;
      return output;
    }
  `

  const fragShaderCode = `
    @group(0) @binding(0) var textureSampler: sampler;
    @group(0) @binding(1) var texture: texture_2d<f32>;

    @fragment
    fn main(@location(0) uv: vec2f) -> @location(0) vec4f {
      let flippedUV = vec2<f32>(uv.x, 1.0 - uv.y);
      return textureSample(texture, textureSampler, flippedUV);
    }
  `

  // 创建渲染管线
  const pipeline = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: device.createShaderModule({ code: vertexShaderCode }),
      entryPoint: 'main',
      buffers: [/* ... */],
    },
    fragment: {
      module: device.createShaderModule({ code: fragShaderCode }),
      entryPoint: 'main',
      targets: [{ format: canvasFormat }],
    },
    primitive: { topology: 'triangle-strip' },
  })

  this.pipeline = pipeline
  this.renderType = 'WEBGPU'
  return true
}

async renderWebgpu() {
  // 1. 在 2D Canvas 上绘制弹幕
  this.renderer.render(this.danmakuList)

  // 2. 将 2D Canvas 内容复制到 GPU 纹理
  this.device.queue.copyExternalImageToTexture(
    { source: this.webgpuCanvas },
    { texture: this.texture },
    { width: this.webgpuCanvas.width, height: this.webgpuCanvas.height },
  )

  // 3. 使用 GPU 渲染到屏幕
  const encoder = this.device.createCommandEncoder()
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
      view: this.context.getCurrentTexture().createView(),
      loadOp: 'clear',
      clearValue: [0, 0, 0, 0],
      storeOp: 'store',
    }],
  })

  pass.setPipeline(this.pipeline)
  pass.setVertexBuffer(0, this.vertexBuffer)
  pass.setBindGroup(0, this.bindGroup)
  pass.draw(4)
  pass.end()

  this.device.queue.submit([encoder.finish()])
}

WebGPU 优势:

  • GPU 加速合成,降低 CPU 负载
  • 更高的渲染性能,支持更大弹幕量
  • 适合高端设备,提供极致体验
3.5 动画循环与性能优化
update = (currentTime) => {
  if (this.state !== 'playing') return

  const elapsed = currentTime - this.lastUpdateTime
  this.lastUpdateTime = currentTime

  // 防止时间跳变(如标签页切换回来)
  if (elapsed <= 0) {
    this.animationId = requestAnimationFrame(this.update)
    return
  }

  // 更新弹幕位置
  this.updateDanmakuX(elapsed)

  // 渲染
  if (this.renderType === 'WEBGPU') {
    this.renderWebgpu()
  } else {
    this.render2D()
  }

  // 尝试从队列中添加弹幕
  this.tryAddPendingDanmaku()

  this.animationId = requestAnimationFrame(this.update)
}

// 更新弹幕位置
updateDanmakuX = (deltaTime) => {
  this.danmakuList = this.danmakuList.filter((danmaku) => {
    // 限制 deltaTime 防止时间跳变导致位置突变
    let _deltaTime = deltaTime
    if (_deltaTime >= 20) {
      _deltaTime = 20
    }
    if (deltaTime < 20 && deltaTime > 15) {
      _deltaTime = 16
    }

    // 滚动弹幕位置更新
    if (danmaku.type === 'scroll' && danmaku.trackId) {
      danmaku.x -= danmaku.speed * (_deltaTime / 1000)
    }

    const isVisible = this.isDanmakuVisible(danmaku)
    if (!isVisible) {
      this.clearCanvas()
    }
    return isVisible
  })
}

// 检查弹幕可见性
isDanmakuVisible(danmaku) {
  if (danmaku.type === 'scroll') {
    // 滚动弹幕:完全离开屏幕左侧才移除
    return danmaku.x + danmaku.boxWidth + SAFE_AREA > 0
  } else {
    // 固定弹幕:根据持续时间判断
    return Date.now() - danmaku.timestamp < danmaku.duration
  }
}

性能优化点:

  1. 时间平滑处理:限制 deltaTime 范围,避免标签页切换导致的位置跳变
  2. 自动清理:及时移除不可见弹幕,减少渲染负担
  3. 按需渲染:只在有弹幕时执行渲染逻辑

四、响应式尺寸适配

updateConfig = ({ newConfig }) => {
  const oldWidth = this.config?.canvasWidth
  const newWidth = newConfig.canvasWidth
  
  // 合并新配置
  this.config = { ...this.config, ...newConfig }

  const newWidthPx = newConfig.canvasWidth * this.config.pixelRatio
  const newHeightPx = newConfig.canvasHeight * this.config.pixelRatio
  
  // 更新画布尺寸
  if (this.offScreenCanvas) {
    this.offScreenCanvas.width = newWidthPx
    this.offScreenCanvas.height = newHeightPx
  }

  // 调整现有弹幕位置
  this.adjustDanmakuX(oldWidth, newWidth, this.danmakuList)
  this.adjustDanmakuX(oldWidth, newWidth, this.penddingList)
}

adjustDanmakuX = (oldWidth, newWidth, danmakuList) => {
  danmakuList.forEach((danmaku) => {
    if (danmaku.type === 'scroll') {
      // 滚动弹幕:保持相对位置
      danmaku.x += (newWidth - oldWidth)
    } else {
      // 固定弹幕:重新居中
      danmaku.x = this.config.canvasWidth / 2 - (danmaku.boxWidth / 2)
    }
  })
}

适配特点:

  • 无缝调整:窗口变化时保持弹幕连续性
  • 位置修正:滚动弹幕保持相对位置,固定弹幕重新居中
  • 双向同步:同时调整屏幕上的弹幕和等待队列

性能对比

指标 Canvas 2D WebGPU
CPU 占用 中等
GPU 占用 中等
最大弹幕量 ~300/s ~800/s
兼容性 99%+ ~70%
适用场景 通用 高端设备

使用示例

<template>
  <DanmakuCanvas
    ref="danmakuRef"
    :show-danmaku="true"
    :emojis="emojiData"
    :config="danmakuConfig"
    @on-error="handleError"
  />
</template>

<script setup>
import { ref } from 'vue'
import DanmakuCanvas from './components/CanvasBarrage/DanmakuCanvas.vue'

const danmakuRef = ref()

const danmakuConfig = {
  fontSize: 20,
  fontFamily: 'PingFang SC',
  color: '#fff',
  duration: 8000,
  trackHeight: 52,
  trackGap: 16,
  trackCount: 3,
  speed: 140,
}

// 发送弹幕
function sendDanmaku(message) {
  danmakuRef.value?.addDanmaku(message, {
    type: 'scroll',      // scroll | fixed
    priority: 0,         // 优先级
    showBorder: false,   // 是否显示边框
  })
}

// 发送 VIP 弹幕
function sendVipDanmaku(message) {
  danmakuRef.value?.addDanmaku(message, {
    type: 'scroll',
    priority: 10,        // 高优先级
    showBorder: true,    // 带边框
    color: '#FFD700',    // 金色
  })
}
</script>

最佳实践

1. 性能监控

// 在 Worker 中上报性能指标
globalThis.postMessage({ 
  type: 'updateHeartDim', 
  data: { 
    key: 'onScreenDanmuCount', 
    value: this.danmakuList.length 
  } 
})

2. 渲染模式选择

// 根据设备能力选择渲染方式
const danmuRenderType = localStorage.getItem('danmuRenderType') || 'webgpu'

// 浏览器支持检测
if (!navigator.gpu) {
  localStorage.setItem('danmuRenderType', 'canvas2d')
  window.location.reload()
}

3. 表情图片预处理

// 使用 ImageBitmap 提升渲染性能
async function loadEmojis(emojiUrls) {
  const emojis = {}
  for (const [key, url] of Object.entries(emojiUrls)) {
    const response = await fetch(url)
    const blob = await response.blob()
    emojis[key] = await createImageBitmap(blob)
  }
  return emojis
}

4. 内存管理

// 限制等待队列长度
const MAX_PENDDING_LIST_LEN = 100

if (this.penddingList.length >= MAX_PENDDING_LIST_LEN) {
  // 丢弃最早的弹幕
  this.penddingList.shift()
  // 上报丢弃数据
  globalThis.postMessage({ 
    type: 'updateHeartDim', 
    data: { key: 'discardDanmuCount', value: 1 } 
  })
}

总结

本文介绍的弹幕系统具备以下特点:

高性能:Web Worker + OffscreenCanvas,不阻塞主线程
可扩展:双渲染引擎,支持渐进增强
智能调度:轨道分配算法 + 优先级队列
功能丰富:富文本、边框、多种弹幕类型
响应式:自适应屏幕尺寸变化
可监控:完善的性能指标上报

这套方案已在生产环境稳定运行,能够支撑高并发直播场景下的大规模弹幕渲染需求。

参考资料

如果这篇文章对你有帮助,欢迎点赞收藏!有任何问题欢迎在评论区讨论~ 🎉

❌
❌