Skip to content

metastable-forge/demo-offscreen-canvas

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Canvas 离屏渲染与多线程性能实验室

这是一个用于深入研究和对比 Canvas 渲染性能优化技术的实验项目。为了更清晰地展示不同阶段的优化策略,我们将实验分为 基础篇进阶篇 两个独立模块。

🎯 项目导航


🧪 基础实验 (Basic Experiments)

核心问题: 当 Canvas 渲染负载过高时,如何避免阻塞 UI 线程?

本模块实现了三种基础渲染模式,代码位于 src/basic/

1. Main Thread (基准模式)

  • 原理: 最朴素的实现方式。每一帧都在主线程中执行:清空画布 -> 重绘复杂背景 -> 计算粒子物理 -> 绘制粒子
  • 表现: 随着负载增加(粒子数增多或背景绘制变慢),FPS 急剧下降。更严重的是,UI 线程被完全阻塞,导致按钮点击无反应、动画停滞、页面无法滚动。
  • 代码位置: src/basic/renderers/MainThreadRenderer.js
sequenceDiagram
    participant UI as UI Thread (Main)

    loop Every Frame
        UI->>UI: Clear Canvas
        UI->>UI: Draw Background (Heavy!)
        UI->>UI: Update Particles
        UI->>UI: Draw Particles
        Note right of UI: UI is blocked during this time
    end
Loading

2. Memory Canvas (内存缓冲模式)

  • 原理: 利用“空间换时间”策略。在初始化时,创建一个内存中的 Canvas (document.createElement('canvas')),将复杂的静态背景绘制一次。后续每一帧只需通过 ctx.drawImage() 将缓存的背景图拷贝到主画布。
  • 注意: 这里的“离屏”是指不在 DOM 树中显示,但它依然运行在主线程。这与 OffscreenCanvas API 是两个不同的概念。
  • 表现: 显著降低了每帧的 CPU 消耗,大幅提升 FPS。但如果动态元素(如粒子)本身的计算量过大,主线程依然会被阻塞。
  • 代码位置: src/basic/renderers/BufferedRenderer.js
sequenceDiagram
    participant UI as UI Thread (Main)
    participant Mem as Memory Canvas

    UI->>Mem: Draw Background (Once)

    loop Every Frame
        UI->>UI: Clear Canvas
        UI->>Mem: Copy Image (Fast!)
        UI->>UI: Update Particles
        UI->>UI: Draw Particles
    end
Loading

3. Worker + OffscreenCanvas (单 Worker 模式)

  • 原理: 利用现代浏览器的 OffscreenCanvas API 和 transferControlToOffscreen() 方法,将 Canvas 的控制权完全转移给 Web Worker 线程。
  • 核心差异: 渲染循环、物理计算、绘图指令全部在 Worker 线程独立运行,与主线程(UI 线程)物理隔离。
  • 表现: 即使 Worker 线程负载极高(FPS 只有 10),主线程依然保持 60 FPS 的响应速度。UI 交互丝滑流畅,完全不受后台渲染任务的影响。
  • 代码位置: src/basic/renderers/WorkerRenderer.js
sequenceDiagram
    participant UI as UI Thread (Main)
    participant Worker as Web Worker

    UI->>Worker: Transfer Control

    par Parallel Execution
        loop UI Loop
            UI->>UI: Handle Clicks/Scrolls
        end

        loop Render Loop
            Worker->>Worker: Draw Background
            Worker->>Worker: Update Particles
            Worker->>Worker: Draw Particles
        end
    end
Loading

对比

特性 Main Thread (基准) Memory Canvas (内存缓冲) Worker +OffscreenCanvas
FPS 表现 ⭐ (差) ⭐⭐⭐ (好) ⭐⭐ (取决于 Worker 负载)
UI 响应性 ❌ 严重阻塞 ⚠️ 可能阻塞 ✅ 完全不阻塞
技术实现 基础 Canvas API document.createElement('canvas') OffscreenCanvas API + Web Worker
线程模型 单线程 (Main) 单线程 (Main) 多线程 (Main + Worker)
兼容性 所有浏览器 所有浏览器 现代浏览器 (Chrome/Edge/Firefox)
适用场景 简单动画、低负载场景 静态背景复杂、动态元素较少的场景 计算密集型、高交互要求的复杂应用

🚀 进阶实验 (Advanced Experiments)

核心问题: 当计算量极大(如物理引擎)需要拆分到独立 Worker 时,如何高效地在 Worker 之间传输数据?

本模块构建了一个 Physics Worker (计算) + Render Worker (渲染) 的双 Worker 架构,对比了四种通信方式。代码位于 src/advanced/

1. Proxy (主线程转发)

  • 流程: Physics Worker -> postMessage -> Main Thread -> postMessage -> Render Worker
  • 原理: 数据经过主线程中转。
  • 评价: ❌ 最慢。不仅有两次拷贝开销(结构化克隆),还会占用主线程时间进行序列化/反序列化,导致 UI 卡顿。这是反面教材。
sequenceDiagram
    participant Phy as Physics Worker
    participant Main as Main Thread
    participant Ren as Render Worker

    loop Every Frame
        Phy->>Phy: Calculate
        Phy->>Main: postMessage(Data)
        Note right of Main: Serialization Cost
        Main->>Ren: postMessage(Data)
        Ren->>Ren: Draw
    end
Loading

2. Channel (直连拷贝)

  • 流程: Physics Worker -> MessageChannel -> Render Worker
  • 原理: 利用 MessageChannel API 建立两个 Worker 之间的直连通道。
  • 评价: ⭐ 中等。成功解放了主线程,UI 不再卡顿。但数据依然需要拷贝,在大数据量下(如 50,000 粒子)会有明显的通信延迟,导致 FPS 上限受限。
sequenceDiagram
    participant Phy as Physics Worker
    participant Ren as Render Worker

    Note over Phy,Ren: Connected via MessageChannel

    loop Every Frame
        Phy->>Phy: Calculate
        Phy->>Ren: postMessage(Data)
        Note right of Phy: Copy Cost
        Ren->>Ren: Draw
    end
Loading

3. Transfer (直连转移)

  • 流程: Physics Worker -> Transferable Objects -> Render Worker
  • 原理: 在 postMessage 时转移数据的所有权(Zero-Copy)。发送方的数据瞬间不可用。
  • 评价: ⭐⭐ 。无拷贝开销。但由于发送方失去了数据,需要实现 Ping-Pong 机制(Render Worker 画完后把数据传回来),逻辑较复杂。
sequenceDiagram
    participant Phy as Physics Worker
    participant Ren as Render Worker

    loop Every Frame
        Phy->>Phy: Calculate
        Phy->>Ren: Transfer(Data)
        Note right of Phy: Ownership Lost
        Ren->>Ren: Draw
        Ren->>Phy: Transfer(Data)
        Note right of Ren: Ownership Returned
    end
Loading

4. Shared (共享内存)

  • 流程: Physics Worker <-> SharedArrayBuffer <-> Render Worker
  • 原理: 主线程创建一块共享内存,两个 Worker 同时读写。
  • 评价: ⭐⭐⭐ 极速。真正的零拷贝,无通信开销(仅需发送微小的同步信号)。这是 Web 前端性能的天花板。
  • 注意: 需要服务器配置 COOP/COEP 响应头(本项目已在 vite.config.js 中配置)。
sequenceDiagram
    participant Phy as Physics Worker
    participant Shared as Shared Memory
    participant Ren as Render Worker

    loop Parallel
        Phy->>Shared: Write Data
        Phy->>Ren: Signal Update
        Ren->>Shared: Read Data
        Ren->>Ren: Draw
    end
Loading

通信模式对比

模式 通信路径 数据传输方式 主线程负载 推荐指数
Proxy Physics -> Main -> Render 两次拷贝 高 (阻塞 UI) ❌ 不推荐
Channel Physics -> Render 一次拷贝 ⭐ 简单场景
Transfer Physics <-> Render 零拷贝 (Ping-Pong) ⭐⭐ 高性能
Shared Physics <-> Render 零拷贝 (SharedArrayBuffer) ⭐⭐⭐ 极限性能

📚 Canvas 渲染最佳实践

基于本项目的实验结果,我们总结了以下指南:

1. 架构选择矩阵

场景特征 推荐方案 理由
简单动画 (< 1000 元素) Main Thread 简单直接,开发成本最低。
复杂静态背景 + 简单动态元素 Memory Canvas 性价比最高,极大降低每帧开销。
海量计算 + 高交互要求 Single Worker 彻底解放 UI 线程,保证交互流畅。
超大规模物理仿真 Multi-Worker (Shared) 利用多核 CPU 并行计算,且无通信瓶颈。

2. 避坑指南

  • 不要混淆概念: document.createElement('canvas') 是内存缓冲,运行在主线程;new OffscreenCanvas() 是新 API,通常配合 Worker 使用。
  • 通信成本: 虽然 Worker 能解放主线程,但频繁的 postMessage 传输大数据也会有性能损耗。尽量使用 Transferable ObjectsSharedArrayBuffer
  • 兼容性: OffscreenCanvasSharedArrayBuffer 在现代浏览器支持良好,但生产环境建议做特性检测。

🛠️ 快速开始

  1. 安装依赖:

    npm install
  2. 启动开发服务器:

    npm run dev

    (注意:进阶实验需要 SharedArrayBuffer,请确保使用 npm run dev 启动以加载 vite.config.js 中的安全配置)

  3. 构建生产版本:

    npm run build

About

一个离屏Canvas实验demos;Canvas Offscreen Rendering and Multithreading Performance Lab

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors