大屏性能优化

技术实现方案

5.1 Canvas分层渲染

原理: 将Canvas分为多个图层,静态内容和动态内容分开渲染, 避免每帧都重绘整个Canvas。

分层策略:

1
2
3
4
图层1 (背景层): 静态背景、网格线 - 只绘制一次
图层2 (内容层): 数据点、图形 - 数据变化时绘制
图层3 (动画层): 粒子、特效 - 每帧绘制
图层4 (交互层): 鼠标hover、tooltip - 交互时绘制
  • 优势:
    • 减少重绘区域
    • 降低CPU占用
    • 提升帧率
    • 优化内存使用

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class LayeredCanvas {
constructor(container) {
this.container = container
this.layers = []
this.init()
}

init() {
// 创建多个Canvas图层
const layerNames = ['background', 'content', 'animation', 'interaction']

layerNames.forEach((name, index) => {
const canvas = document.createElement('canvas')
canvas.style.position = 'absolute'
canvas.style.left = '0'
canvas.style.top = '0'
canvas.style.zIndex = index
canvas.width = this.container.offsetWidth
canvas.height = this.container.offsetHeight

this.container.appendChild(canvas)

this.layers.push({
name,
canvas,
ctx: canvas.getContext('2d'),
dirty: true
})
})
}

getLayer(name) {
return this.layers.find(layer => layer.name === name)
}

markDirty(name) {
const layer = this.getLayer(name)
if (layer) {
layer.dirty = true
}
}

render() {
this.layers.forEach(layer => {
if (layer.dirty) {
// 只重绘dirty的图层
this.renderLayer(layer)
layer.dirty = false
}
})
}

renderLayer(layer) {
// 子类实现具体绘制逻辑
}
}

5.2 离屏渲染优化

概念: 将复杂图形先绘制到不可见的离屏Canvas, 然后一次性绘制到主Canvas,减少重复计算。

适用场景:

  • 复杂的几何图形
  • 重复使用的图案
  • 静态的装饰元素
  • 文字渲染

性能对比:

1
2
3
普通渲染: 每帧计算+绘制 (10ms+5ms=15ms/帧)
离屏渲染: 预计算一次+每帧绘制 (10ms+0.5ms=0.5ms/帧)
性能提升: 30倍

实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class OffscreenRenderer {
constructor() {
this.cache = new Map()
}

// 预渲染到离屏Canvas
preRender(key, width, height, drawFn) {
const offscreen = document.createElement('canvas')
offscreen.width = width
offscreen.height = height
const ctx = offscreen.getContext('2d')

drawFn(ctx)

this.cache.set(key, offscreen)
}

// 使用缓存的离屏Canvas
render(ctx, key, x, y) {
const offscreen = this.cache.get(key)
if (offscreen) {
ctx.drawImage(offscreen, x, y)
}
}

// 清除缓存
clear(key) {
if (key) {
this.cache.delete(key)
} else {
this.cache.clear()
}
}
}

5.3 脏矩形检测

原理: 只重绘发生变化的矩形区域,而不是整个Canvas。

检测算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class DirtyRectDetector {
constructor() {
this.dirtyRects = []
}

// 添加脏矩形
addDirty(x, y, width, height) {
this.dirtyRects.push({ x, y, width, height })
}

// 合并重叠的矩形
merge() {
// 简化版: 计算包含所有脏矩形的最小矩形
if (this.dirtyRects.length === 0) return null

let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity

this.dirtyRects.forEach(rect => {
minX = Math.min(minX, rect.x)
minY = Math.min(minY, rect.y)
maxX = Math.max(maxX, rect.x + rect.width)
maxY = Math.max(maxY, rect.y + rect.height)
})

return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
}

// 清空脏矩形
clear() {
this.dirtyRects = []
}

// 渲染脏区域
render(ctx, renderFn) {
const dirtyRect = this.merge()

if (dirtyRect) {
// 只清除和重绘脏区域
ctx.save()
ctx.beginPath()
ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
ctx.clip()

ctx.clearRect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
renderFn(ctx, dirtyRect)

ctx.restore()
}

this.clear()
}
}

性能提升: 假设Canvas 1920x1080,移动的对象100x100:

  • 全量渲染: 2,073,600像素
  • 脏矩形: 10,000像素
  • 性能提升: 207倍

5.4 Web Worker计算

使用场景:

  • 大数据计算
  • 复杂算法
  • 数据处理
  • 图像处理

通信模式:

1
2
主线程 → Worker: postMessage(data)
Worker → 主线程: postMessage(result)

实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// main.js
const worker = new Worker('worker.js')

worker.postMessage({
type: 'calculate',
data: largeDataArray
})

worker.onmessage = (e) => {
const result = e.data
updateChart(result)
}

// worker.js
self.onmessage = (e) => {
const { type, data } = e.data

if (type === 'calculate') {
// 耗时计算
const result = complexCalculation(data)

self.postMessage(result)
}
}

function complexCalculation(data) {
// 大数据处理
return data.map(item => {
// 复杂运算...
return processedItem
})
}

优势:

  • 不阻塞主线程
  • 充分利用多核CPU
  • 提升响应速度
  • 改善用户体验

注意事项:

  • Worker无法访问DOM
  • 数据传输有序列化开销
  • 适合CPU密集型任务
  • 不适合频繁通信

5.5 GPU加速

原理: 使用transform、opacity等GPU加速的CSS属性, 将渲染工作交给GPU而非CPU。

GPU加速属性:

1
2
3
4
5
6
7
8
9
10
11
/* 推荐使用 */
transform: translate3d(x, y, 0);
transform: scale(1.2);
transform: rotate(45deg);
opacity: 0.8;
filter: blur(5px);

/* 避免使用 */
left: 100px; /* 触发layout */
width: 200px; /* 触发layout */
margin: 10px; /* 触发layout */

开启GPU加速:

1
2
3
4
5
6
7
.accelerated {
transform: translateZ(0);
/* 或 */
will-change: transform;
/* 或 */
backface-visibility: hidden;
}

性能对比:

属性 触发 性能
transform Composite 最快
opacity Paint+Composite
color Paint+Composite
width/height Layout+Paint+Composite

will-change使用:

1
2
3
4
5
6
7
8
9
/* 提前声明将要变化的属性 */
.element {
will-change: transform, opacity;
}

/* 动画结束后移除 */
.element.animated {
will-change: auto;
}

注意事项:

  • 不要滥用,每个图层消耗内存
  • 移动设备GPU性能有限
  • 过多图层反而降低性能
  • 合理使用,按需开启

可运行代码Demo

Demo 1: Canvas分层渲染完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
<template>
<div class="layered-canvas-demo">
<div class="controls">
<button @click="toggleAnimation">
{{ isAnimating ? '暂停动画' : '开始动画' }}
</button>
<button @click="addParticles">添加粒子</button>
<button @click="clearParticles">清除粒子</button>
<div class="stats">
<span>FPS: {{ fps }}</span>
<span>粒子数: {{ particleCount }}</span>
</div>
</div>

<div ref="canvasContainer" class="canvas-container"></div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 粒子类
class Particle {
constructor(x, y) {
this.x = x
this.y = y
this.vx = (Math.random() - 0.5) * 4
this.vy = (Math.random() - 0.5) * 4
this.radius = Math.random() * 3 + 1
this.color = `hsl(${Math.random() * 60 + 180}, 100%, 50%)`
}

update(width, height) {
this.x += this.vx
this.y += this.vy

if (this.x < 0 || this.x > width) this.vx *= -1
if (this.y < 0 || this.y > height) this.vy *= -1
}

draw(ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
ctx.fillStyle = this.color
ctx.fill()
}
}

// 分层Canvas管理器
class LayeredCanvasManager {
constructor(container) {
this.container = container
this.layers = {}
this.particles = []
this.animationId = null
this.isAnimating = false
this.lastTime = 0
this.fps = 60
this.frameCount = 0
this.fpsUpdateTime = 0

this.init()
}

init() {
const width = this.container.offsetWidth
const height = this.container.offsetHeight

// 创建图层
const layerNames = ['background', 'grid', 'animation', 'overlay']

layerNames.forEach((name, index) => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
canvas.style.position = 'absolute'
canvas.style.left = '0'
canvas.style.top = '0'
canvas.style.zIndex = index

this.container.appendChild(canvas)

this.layers[name] = {
canvas,
ctx: canvas.getContext('2d'),
dirty: true
}
})

this.renderStatic()
}

// 渲染静态图层(只绘制一次)
renderStatic() {
this.renderBackground()
this.renderGrid()
}

renderBackground() {
const { ctx, canvas } = this.layers.background

// 渐变背景
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
gradient.addColorStop(0, '#0a0e27')
gradient.addColorStop(1, '#1a1f3a')

ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)

this.layers.background.dirty = false
}

renderGrid() {
const { ctx, canvas } = this.layers.grid

ctx.strokeStyle = 'rgba(0, 246, 255, 0.1)'
ctx.lineWidth = 1

// 绘制网格
const gridSize = 50

for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.stroke()
}

for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(canvas.width, y)
ctx.stroke()
}

this.layers.grid.dirty = false
}

// 渲染动画图层(每帧绘制)
renderAnimation() {
const { ctx, canvas } = this.layers.animation

// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)

// 更新和绘制粒子
this.particles.forEach(particle => {
particle.update(canvas.width, canvas.height)
particle.draw(ctx)
})
}

// 渲染覆盖层(显示信息)
renderOverlay() {
const { ctx, canvas } = this.layers.overlay

ctx.clearRect(0, 0, canvas.width, canvas.height)

// 显示图层信息
ctx.fillStyle = '#00f6ff'
ctx.font = '14px monospace'
ctx.fillText('分层渲染演示', 10, 20)
ctx.fillText(`图层: ${Object.keys(this.layers).length}`, 10, 40)
ctx.fillText(`粒子: ${this.particles.length}`, 10, 60)
}

addParticles(count = 50) {
const { canvas } = this.layers.animation

for (let i = 0; i < count; i++) {
const x = Math.random() * canvas.width
const y = Math.random() * canvas.height
this.particles.push(new Particle(x, y))
}
}

clearParticles() {
this.particles = []
}

start() {
if (this.isAnimating) return

this.isAnimating = true
this.lastTime = performance.now()
this.animate()
}

stop() {
this.isAnimating = false
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}

animate(currentTime = 0) {
if (!this.isAnimating) return

// 计算FPS
this.frameCount++
if (currentTime - this.fpsUpdateTime > 1000) {
this.fps = this.frameCount
this.frameCount = 0
this.fpsUpdateTime = currentTime
}

// 渲染
this.renderAnimation()
this.renderOverlay()

this.animationId = requestAnimationFrame((time) => this.animate(time))
}

destroy() {
this.stop()
Object.values(this.layers).forEach(layer => {
layer.canvas.remove()
})
}
}

// 组件状态
const canvasContainer = ref(null)
let manager = null
const isAnimating = ref(false)
const fps = ref(60)
const particleCount = ref(0)

// FPS更新
setInterval(() => {
if (manager) {
fps.value = manager.fps
particleCount.value = manager.particles.length
}
}, 100)

const toggleAnimation = () => {
if (!manager) return

if (isAnimating.value) {
manager.stop()
} else {
manager.start()
}

isAnimating.value = !isAnimating.value
}

const addParticles = () => {
if (manager) {
manager.addParticles(50)
}
}

const clearParticles = () => {
if (manager) {
manager.clearParticles()
}
}

onMounted(() => {
manager = new LayeredCanvasManager(canvasContainer.value)
manager.addParticles(100)
manager.start()
isAnimating.value = true
})

onUnmounted(() => {
if (manager) {
manager.destroy()
}
})
</script>

<style scoped>
.layered-canvas-demo {
padding: 20px;
background: #0a0e27;
border-radius: 10px;
color: #fff;
}

.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}

.controls button {
padding: 10px 20px;
background: #00f6ff;
border: none;
border-radius: 5px;
color: #000;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}

.controls button:hover {
background: #00d9e6;
transform: translateY(-2px);
}

.stats {
margin-left: auto;
display: flex;
gap: 20px;
color: #00f6ff;
font-family: monospace;
}

.canvas-container {
position: relative;
width: 100%;
height: 500px;
border: 2px solid #00f6ff;
border-radius: 8px;
overflow: hidden;
}
</style>

Demo 2: 离屏渲染优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
<template>
<div class="offscreen-demo">
<div class="mode-selector">
<label>
<input type="radio" value="normal" v-model="renderMode" />
普通渲染
</label>
<label>
<input type="radio" value="offscreen" v-model="renderMode" />
离屏渲染
</label>
</div>

<div class="performance-stats">
<div class="stat">
<span>FPS:</span>
<strong :class="fpsClass">{{ fps }}</strong>
</div>
<div class="stat">
<span>渲染时间:</span>
<strong>{{ renderTime }}ms</strong>
</div>
<div class="stat">
<span>模式:</span>
<strong>{{ renderModeName }}</strong>
</div>
</div>

<canvas ref="mainCanvas" class="main-canvas"></canvas>
</div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'

const mainCanvas = ref(null)
const renderMode = ref('offscreen')
const fps = ref(60)
const renderTime = ref(0)

let ctx = null
let offscreenCanvas = null
let offscreenCtx = null
let animationId = null
let frameCount = 0
let lastTime = 0
let fpsTime = 0

// 渲染模式名称
const renderModeName = computed(() => {
return renderMode.value === 'offscreen' ? '离屏渲染' : '普通渲染'
})

// FPS样式
const fpsClass = computed(() => {
if (fps.value >= 55) return 'good'
if (fps.value >= 30) return 'medium'
return 'bad'
})

// 绘制复杂图形
function drawComplexShape(context, x, y, size) {
const startTime = performance.now()

context.save()
context.translate(x, y)

// 绘制复杂的星形
context.beginPath()
for (let i = 0; i < 10; i++) {
const angle = (i * Math.PI * 2) / 10
const radius = i % 2 === 0 ? size : size / 2
const px = Math.cos(angle) * radius
const py = Math.sin(angle) * radius

if (i === 0) {
context.moveTo(px, py)
} else {
context.lineTo(px, py)
}
}
context.closePath()

// 渐变填充
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, size)
gradient.addColorStop(0, '#00f6ff')
gradient.addColorStop(1, '#0066cc')
context.fillStyle = gradient
context.fill()

// 描边
context.strokeStyle = '#00ffff'
context.lineWidth = 2
context.stroke()

context.restore()

return performance.now() - startTime
}

// 普通渲染
function normalRender() {
const canvas = mainCanvas.value
const width = canvas.width
const height = canvas.height

ctx.clearRect(0, 0, width, height)

let totalTime = 0

// 绘制100个复杂图形
for (let i = 0; i < 100; i++) {
const x = (i % 10) * (width / 10) + width / 20
const y = Math.floor(i / 10) * (height / 10) + height / 20
const size = 30

totalTime += drawComplexShape(ctx, x, y, size)
}

renderTime.value = totalTime.toFixed(2)
}

// 离屏渲染
function offscreenRender() {
const canvas = mainCanvas.value
const width = canvas.width
const height = canvas.height

ctx.clearRect(0, 0, width, height)

const startTime = performance.now()

// 直接绘制预渲染的离屏Canvas
for (let i = 0; i < 100; i++) {
const x = (i % 10) * (width / 10) + width / 20
const y = Math.floor(i / 10) * (height / 10) + height / 20

ctx.drawImage(offscreenCanvas, x - 30, y - 30)
}

renderTime.value = (performance.now() - startTime).toFixed(2)
}

// 动画循环
function animate(currentTime) {
frameCount++

// 计算FPS
if (currentTime - fpsTime > 1000) {
fps.value = frameCount
frameCount = 0
fpsTime = currentTime
}

// 根据模式选择渲染方法
if (renderMode.value === 'offscreen') {
offscreenRender()
} else {
normalRender()
}

animationId = requestAnimationFrame(animate)
}

// 初始化离屏Canvas
function initOffscreen() {
offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = 60
offscreenCanvas.height = 60
offscreenCtx = offscreenCanvas.getContext('2d')

// 预渲染复杂图形到离屏Canvas
drawComplexShape(offscreenCtx, 30, 30, 30)
}

// 监听模式切换
watch(renderMode, () => {
fps.value = 60
frameCount = 0
})

onMounted(() => {
const canvas = mainCanvas.value
canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight
ctx = canvas.getContext('2d')

initOffscreen()

lastTime = performance.now()
fpsTime = lastTime
animate(lastTime)
})

onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
})
</script>

<style scoped>
.offscreen-demo {
padding: 20px;
background: #0a0e27;
border-radius: 10px;
color: #fff;
}

.mode-selector {
display: flex;
gap: 20px;
margin-bottom: 15px;
}

.mode-selector label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 15px;
background: rgba(0, 246, 255, 0.1);
border-radius: 5px;
transition: all 0.3s ease;
}

.mode-selector label:hover {
background: rgba(0, 246, 255, 0.2);
}

.mode-selector input[type="radio"] {
cursor: pointer;
}

.performance-stats {
display: flex;
gap: 30px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
margin-bottom: 15px;
}

.stat {
display: flex;
gap: 10px;
align-items: center;
}

.stat span {
color: #aaa;
}

.stat strong {
color: #00f6ff;
font-size: 20px;
}

.stat strong.good {
color: #4caf50;
}

.stat strong.medium {
color: #ff9800;
}

.stat strong.bad {
color: #f44336;
}

.main-canvas {
width: 100%;
height: 500px;
background: rgba(0, 0, 0, 0.3);
border: 2px solid #00f6ff;
border-radius: 8px;
}
</style>

Demo 3: Web Worker数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<template>
<div class="worker-demo">
<div class="controls">
<button @click="processInMain">主线程处理</button>
<button @click="processInWorker">Worker处理</button>
<button @click="generateData">生成数据</button>
</div>

<div class="status-panel">
<div class="status-item">
<span>数据量:</span>
<strong>{{ dataSize }}</strong>
</div>
<div class="status-item">
<span>处理状态:</span>
<strong :class="statusClass">{{ status }}</strong>
</div>
<div class="status-item">
<span>耗时:</span>
<strong>{{ processingTime }}ms</strong>
</div>
<div class="status-item">
<span>UI响应:</span>
<strong :class="responseClass">{{ uiResponse }}</strong>
</div>
</div>

<div class="test-area">
<h4>UI响应测试</h4>
<button @click="testCount++">点击测试 ({{ testCount }})</button>
<p>在数据处理过程中点击此按钮测试UI是否响应</p>
</div>

<div class="result-area">
<h4>处理结果</h4>
<pre>{{ resultPreview }}</pre>
</div>
</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const dataSize = ref(0)
const status = ref('空闲')
const processingTime = ref(0)
const uiResponse = ref('正常')
const testCount = ref(0)
const result = ref(null)

// 状态样式
const statusClass = computed(() => {
return {
'status-idle': status.value === '空闲',
'status-processing': status.value === '处理中',
'status-done': status.value === '完成'
}
})

// 响应样式
const responseClass = computed(() => {
return {
'response-good': uiResponse.value === '正常',
'response-bad': uiResponse.value === '阻塞'
}
})

// 结果预览
const resultPreview = computed(() => {
if (!result.value) return '暂无结果'

const preview = result.value.slice(0, 5)
return JSON.stringify(preview, null, 2) + '\n...'
})

// 生成测试数据
const generateData = () => {
const size = 1000000
dataSize.value = size
status.value = '空闲'
processingTime.value = 0
result.value = null
}

// 复杂计算函数
function complexCalculation(data) {
return data.map((item, index) => {
// 模拟复杂计算
let result = item
for (let i = 0; i < 100; i++) {
result = Math.sqrt(result * result + i)
}
return {
index,
original: item,
processed: result,
timestamp: Date.now()
}
})
}

// 主线程处理
const processInMain = () => {
if (!dataSize.value) {
alert('请先生成数据')
return
}

status.value = '处理中'
uiResponse.value = '阻塞'

// 生成数据
const data = Array.from({ length: dataSize.value }, () => Math.random() * 100)

const startTime = performance.now()

// 在主线程执行计算(会阻塞UI)
setTimeout(() => {
const processed = complexCalculation(data)

processingTime.value = Math.round(performance.now() - startTime)
result.value = processed
status.value = '完成'
uiResponse.value = '正常'
}, 10)
}

// Worker处理
const processInWorker = () => {
if (!dataSize.value) {
alert('请先生成数据')
return
}

status.value = '处理中'
uiResponse.value = '正常'

// 生成数据
const data = Array.from({ length: dataSize.value }, () => Math.random() * 100)

const startTime = performance.now()

// 创建Worker
const workerCode = `
self.onmessage = function(e) {
const data = e.data

const result = data.map((item, index) => {
let result = item
for (let i = 0; i < 100; i++) {
result = Math.sqrt(result * result + i)
}
return {
index,
original: item,
processed: result,
timestamp: Date.now()
}
})

self.postMessage(result)
}
`

const blob = new Blob([workerCode], { type: 'application/javascript' })
const workerUrl = URL.createObjectURL(blob)
const worker = new Worker(workerUrl)

worker.postMessage(data)

worker.onmessage = (e) => {
processingTime.value = Math.round(performance.now() - startTime)
result.value = e.data
status.value = '完成'

worker.terminate()
URL.revokeObjectURL(workerUrl)
}

worker.onerror = (error) => {
console.error('Worker错误:', error)
status.value = '错误'
worker.terminate()
}
}

// 初始化
generateData()
</script>

<style scoped>
.worker-demo {
padding: 20px;
background: #0a0e27;
border-radius: 10px;
color: #fff;
}

.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
}

.controls button {
padding: 10px 20px;
background: #00f6ff;
border: none;
border-radius: 5px;
color: #000;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}

.controls button:hover {
background: #00d9e6;
transform: translateY(-2px);
}

.status-panel {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
padding: 15px;
background: rgba(0, 246, 255, 0.1);
border-radius: 8px;
margin-bottom: 15px;
}

.status-item {
text-align: center;
}

.status-item span {
display: block;
color: #aaa;
font-size: 12px;
margin-bottom: 5px;
}

.status-item strong {
display: block;
font-size: 18px;
}

.status-idle {
color: #666;
}

.status-processing {
color: #ff9800;
}

.status-done {
color: #4caf50;
}

.response-good {
color: #4caf50;
}

.response-bad {
color: #f44336;
}

.test-area {
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
margin-bottom: 15px;
}

.test-area h4 {
color: #00f6ff;
margin-bottom: 10px;
}

.test-area button {
padding: 8px 16px;
background: #4caf50;
border: none;
border-radius: 5px;
color: #fff;
font-weight: bold;
cursor: pointer;
margin-bottom: 10px;
}

.test-area p {
color: #aaa;
font-size: 14px;
margin: 0;
}

.result-area {
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
}

.result-area h4 {
color: #00f6ff;
margin-bottom: 10px;
}

.result-area pre {
background: rgba(0, 0, 0, 0.5);
padding: 15px;
border-radius: 5px;
color: #00f6ff;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
margin: 0;
}
</style>

Demo 4: GPU加速动画对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
<template>
<div class="gpu-demo">
<div class="mode-selector">
<h4>选择动画方式:</h4>
<label>
<input type="checkbox" v-model="useTransform" />
Transform (GPU加速)
</label>
<label>
<input type="checkbox" v-model="usePosition" />
Left/Top (CPU)
</label>
</div>

<div class="performance-monitor">
<div class="monitor-item">
<span>FPS:</span>
<strong :class="fpsClass">{{ fps }}</strong>
</div>
<div class="monitor-item">
<span>CPU占用:</span>
<strong>{{ cpuUsage }}%</strong>
</div>
<div class="monitor-item">
<span>动画元素:</span>
<strong>{{ elementCount }}</strong>
</div>
</div>

<div class="animation-container" ref="container">
<div
v-for="i in elementCount"
:key="i"
class="animated-box"
:class="{
'use-transform': useTransform,
'use-position': usePosition
}"
:style="getBoxStyle(i)"
></div>
</div>

<div class="explanation">
<h4>性能对比说明:</h4>
<ul>
<li>Transform: 使用GPU加速,性能好,帧率高</li>
<li>Left/Top: 触发Layout,性能差,帧率低</li>
<li>建议: 动画优先使用transform和opacity</li>
</ul>
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const useTransform = ref(true)
const usePosition = ref(false)
const fps = ref(60)
const cpuUsage = ref(0)
const elementCount = 100
const container = ref(null)

let animationId = null
let frameCount = 0
let lastTime = 0
let fpsTime = 0

// FPS样式
const fpsClass = computed(() => {
if (fps.value >= 55) return 'good'
if (fps.value >= 30) return 'medium'
return 'bad'
})

// 获取盒子样式
const getBoxStyle = (index) => {
const angle = (index / elementCount) * Math.PI * 2
const radius = 200
const x = Math.cos(angle) * radius
const y = Math.sin(angle) * radius

return {
'--x': x + 'px',
'--y': y + 'px',
'--delay': (index * 0.02) + 's'
}
}

// 模拟CPU占用
const updateCPUUsage = () => {
let usage = 10

if (useTransform.value && !usePosition.value) {
usage = Math.random() * 10 + 5 // 5-15%
} else if (usePosition.value && !useTransform.value) {
usage = Math.random() * 40 + 40 // 40-80%
} else if (useTransform.value && usePosition.value) {
usage = Math.random() * 30 + 50 // 50-80%
}

cpuUsage.value = Math.round(usage)
}

// 动画循环
const animate = (currentTime) => {
frameCount++

// 计算FPS
if (currentTime - fpsTime > 1000) {
fps.value = frameCount
frameCount = 0
fpsTime = currentTime
updateCPUUsage()
}

animationId = requestAnimationFrame(animate)
}

onMounted(() => {
lastTime = performance.now()
fpsTime = lastTime
animate(lastTime)
})

onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
})
</script>

<style scoped>
.gpu-demo {
padding: 20px;
background: #0a0e27;
border-radius: 10px;
color: #fff;
}

.mode-selector {
margin-bottom: 15px;
}

.mode-selector h4 {
color: #00f6ff;
margin-bottom: 10px;
}

.mode-selector label {
display: inline-flex;
align-items: center;
gap: 8px;
margin-right: 20px;
padding: 8px 15px;
background: rgba(0, 246, 255, 0.1);
border-radius: 5px;
cursor: pointer;
}

.performance-monitor {
display: flex;
gap: 30px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
margin-bottom: 15px;
}

.monitor-item {
display: flex;
gap: 10px;
align-items: center;
}

.monitor-item span {
color: #aaa;
}

.monitor-item strong {
color: #00f6ff;
font-size: 20px;
}

.monitor-item strong.good {
color: #4caf50;
}

.monitor-item strong.medium {
color: #ff9800;
}

.monitor-item strong.bad {
color: #f44336;
}

.animation-container {
position: relative;
width: 100%;
height: 500px;
background: rgba(0, 0, 0, 0.3);
border: 2px solid #00f6ff;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
}

.animated-box {
position: absolute;
width: 20px;
height: 20px;
background: #00f6ff;
border-radius: 50%;
top: 50%;
left: 50%;
}

/* GPU加速 - Transform */
.animated-box.use-transform {
animation: rotate-transform 3s linear infinite;
animation-delay: var(--delay);
transform: translate(var(--x), var(--y)) translateZ(0);
will-change: transform;
}

@keyframes rotate-transform {
from {
transform: translate(var(--x), var(--y)) rotate(0deg) translateZ(0);
}
to {
transform: translate(var(--x), var(--y)) rotate(360deg) translateZ(0);
}
}

/* CPU渲染 - Left/Top */
.animated-box.use-position {
animation: rotate-position 3s linear infinite;
animation-delay: var(--delay);
left: calc(50% + var(--x));
top: calc(50% + var(--y));
}

@keyframes rotate-position {
0% { margin-left: 0; margin-top: 0; }
25% { margin-left: 50px; margin-top: 0; }
50% { margin-left: 50px; margin-top: 50px; }
75% { margin-left: 0; margin-top: 50px; }
100% { margin-left: 0; margin-top: 0; }
}

.explanation {
padding: 15px;
background: rgba(0, 246, 255, 0.1);
border-radius: 8px;
}

.explanation h4 {
color: #00f6ff;
margin-bottom: 10px;
}

.explanation ul {
margin: 0;
padding-left: 20px;
color: #aaa;
}

.explanation li {
margin-bottom: 8px;
line-height: 1.6;
}
</style>