脏矩形优化

深入学习Canvas脏矩形技术,通过局部重绘减少重绘区域提升性能 脏矩形(Dirty Rectangle)技术是一种重要的Canvas性能优化方法,通过只重绘发生变化的区域,大幅减少不必要的像素处理。

基本概念

什么是脏矩形

脏矩形是指需要重绘的矩形区域。通过跟踪变化区域,只清除和重绘这些区域,而不是整个Canvas。

class DirtyRectangle {
  constructor() {
    this.rects = []
    this.mergedRect = null
  }
  
  add(x, y, width, height, padding = 0) {
    this.rects.push({
      x: x - padding,
      y: y - padding,
      width: width + padding * 2,
      height: height + padding * 2
    })
    this.mergedRect = null
  }
  
  addFromBounds(bounds, padding = 0) {
    this.add(
      bounds.x, bounds.y,
      bounds.width, bounds.height,
      padding
    )
  }
  
  merge() {
    if (this.rects.length === 0) {
      return null
    }
    
    if (this.mergedRect) {
      return this.mergedRect
    }
    
    let minX = Infinity
    let minY = Infinity
    let maxX = -Infinity
    let maxY = -Infinity
    
    this.rects.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)
    })
    
    this.mergedRect = {
      x: minX,
      y: minY,
      width: maxX - minX,
      height: maxY - minY
    }
    
    return this.mergedRect
  }
  
  clear() {
    this.rects = []
    this.mergedRect = null
  }
  
  isEmpty() {
    return this.rects.length === 0
  }
  
  getRects() {
    return [...this.rects]
  }
}

脏矩形渲染器

基础脏矩形渲染

class DirtyRectangleRenderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.dirtyRects = new DirtyRectangle()
    this.objects = []
    this.background = null
  }
  
  setBackground(drawFn) {
    this.background = drawFn
  }
  
  addObject(obj) {
    this.objects.push(obj)
    this.markDirty(obj)
  }
  
  removeObject(obj) {
    const index = this.objects.indexOf(obj)
    if (index > -1) {
      this.markDirty(obj)
      this.objects.splice(index, 1)
    }
  }
  
  updateObject(obj, oldBounds) {
    if (oldBounds) {
      this.dirtyRects.addFromBounds(oldBounds, 5)
    }
    this.markDirty(obj)
  }
  
  markDirty(obj) {
    if (obj.getBounds) {
      this.dirtyRects.addFromBounds(obj.getBounds(), 5)
    } else {
      this.dirtyRects.add(obj.x, obj.y, obj.width || 0, obj.height || 0, 5)
    }
  }
  
  render() {
    const dirtyRect = this.dirtyRects.merge()
    
    if (!dirtyRect) {
      return
    }
    
    this.ctx.save()
    
    this.ctx.beginPath()
    this.ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
    this.ctx.clip()
    
    this.ctx.clearRect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
    
    if (this.background) {
      this.background(this.ctx, dirtyRect)
    }
    
    this.objects.forEach(obj => {
      if (this.intersects(obj, dirtyRect)) {
        obj.render(this.ctx)
      }
    })
    
    this.ctx.restore()
    
    this.dirtyRects.clear()
  }
  
  intersects(obj, rect) {
    const bounds = obj.getBounds ? obj.getBounds() : obj
    return bounds.x < rect.x + rect.width &&
           bounds.x + (bounds.width || 0) > rect.x &&
           bounds.y < rect.y + rect.height &&
           bounds.y + (bounds.height || 0) > rect.y
  }
  
  renderAll() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    
    if (this.background) {
      this.background(this.ctx, null)
    }
    
    this.objects.forEach(obj => obj.render(this.ctx))
    
    this.dirtyRects.clear()
  }
}

多矩形优化

class MultiDirtyRectRenderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.dirtyRects = []
    this.objects = []
    this.maxRects = 10
    this.mergeThreshold = 0.7
  }
  
  addDirtyRect(x, y, width, height) {
    this.dirtyRects.push({ x, y, width, height })
    
    if (this.dirtyRects.length > this.maxRects) {
      this.optimizeRects()
    }
  }
  
  optimizeRects() {
    const totalArea = this.dirtyRects.reduce((sum, r) => sum + r.width * r.height, 0)
    const merged = this.mergeAllRects()
    const mergedArea = merged.width * merged.height
    
    if (mergedArea < totalArea * this.mergeThreshold) {
      this.dirtyRects = [merged]
    }
  }
  
  mergeAllRects() {
    let minX = Infinity, minY = Infinity
    let maxX = -Infinity, maxY = -Infinity
    
    this.dirtyRects.forEach(r => {
      minX = Math.min(minX, r.x)
      minY = Math.min(minY, r.y)
      maxX = Math.max(maxX, r.x + r.width)
      maxY = Math.max(maxY, r.y + r.height)
    })
    
    return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
  }
  
  render() {
    if (this.dirtyRects.length === 0) return
    
    this.dirtyRects.forEach(rect => {
      this.ctx.save()
      
      this.ctx.beginPath()
      this.ctx.rect(rect.x, rect.y, rect.width, rect.height)
      this.ctx.clip()
      
      this.ctx.clearRect(rect.x, rect.y, rect.width, rect.height)
      
      this.objects.forEach(obj => {
        if (this.intersects(obj, rect)) {
          obj.render(this.ctx)
        }
      })
      
      this.ctx.restore()
    })
    
    this.dirtyRects = []
  }
  
  intersects(obj, rect) {
    return obj.x < rect.x + rect.width &&
           obj.x + obj.width > rect.x &&
           obj.y < rect.y + rect.height &&
           obj.y + obj.height > rect.y
  }
}

对象变化追踪

自动脏区域检测

class TrackedObject {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this._prevX = x
    this._prevY = y
    this._prevWidth = width
    this._prevHeight = height
    this.dirtyCallback = null
  }
  
  setDirtyCallback(callback) {
    this.dirtyCallback = callback
  }
  
  setPosition(x, y) {
    if (this.x !== x || this.y !== y) {
      this.notifyDirty()
      this._prevX = this.x
      this._prevY = this.y
      this.x = x
      this.y = y
    }
  }
  
  setSize(width, height) {
    if (this.width !== width || this.height !== height) {
      this.notifyDirty()
      this._prevWidth = this.width
      this._prevHeight = this.height
      this.width = width
      this.height = height
    }
  }
  
  notifyDirty() {
    if (this.dirtyCallback) {
      this.dirtyCallback(this.getPreviousBounds())
      this.dirtyCallback(this.getBounds())
    }
  }
  
  getBounds() {
    return { x: this.x, y: this.y, width: this.width, height: this.height }
  }
  
  getPreviousBounds() {
    return {
      x: this._prevX,
      y: this._prevY,
      width: this._prevWidth,
      height: this._prevHeight
    }
  }
  
  render(ctx) {
    ctx.fillStyle = '#e94560'
    ctx.fillRect(this.x, this.y, this.width, this.height)
  }
}

class TrackedRenderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.dirtyRects = new DirtyRectangle()
    this.objects = new Set()
  }
  
  addObject(obj) {
    this.objects.add(obj)
    obj.setDirtyCallback((bounds) => {
      if (bounds) {
        this.dirtyRects.addFromBounds(bounds, 5)
      }
    })
    this.dirtyRects.addFromBounds(obj.getBounds(), 5)
  }
  
  removeObject(obj) {
    this.dirtyRects.addFromBounds(obj.getBounds(), 5)
    obj.setDirtyCallback(null)
    this.objects.delete(obj)
  }
  
  render() {
    const dirtyRect = this.dirtyRects.merge()
    
    if (!dirtyRect) return
    
    this.ctx.save()
    this.ctx.beginPath()
    this.ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
    this.ctx.clip()
    
    this.ctx.clearRect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
    
    this.objects.forEach(obj => {
      if (this.intersects(obj, dirtyRect)) {
        obj.render(this.ctx)
      }
    })
    
    this.ctx.restore()
    this.dirtyRects.clear()
  }
  
  intersects(obj, rect) {
    const bounds = obj.getBounds()
    return bounds.x < rect.x + rect.width &&
           bounds.x + bounds.width > rect.x &&
           bounds.y < rect.y + rect.height &&
           bounds.y + bounds.height > rect.y
  }
}

背景缓存

背景预渲染

class CachedBackgroundRenderer {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    this.dirtyRects = new DirtyRectangle()
    this.objects = []
    
    this.backgroundCanvas = null
    this.backgroundCtx = null
    this.backgroundValid = false
    
    this.initBackground()
  }
  
  initBackground() {
    this.backgroundCanvas = document.createElement('canvas')
    this.backgroundCanvas.width = this.canvas.width
    this.backgroundCanvas.height = this.canvas.height
    this.backgroundCtx = this.backgroundCanvas.getContext('2d')
  }
  
  setBackground(renderFn) {
    this.backgroundRenderFn = renderFn
    this.backgroundValid = false
    this.renderBackground()
  }
  
  renderBackground() {
    if (!this.backgroundValid && this.backgroundRenderFn) {
      this.backgroundCtx.clearRect(0, 0, this.backgroundCanvas.width, this.backgroundCanvas.height)
      this.backgroundRenderFn(this.backgroundCtx)
      this.backgroundValid = true
    }
  }
  
  addObject(obj) {
    this.objects.push(obj)
    this.markDirty(obj.getBounds())
  }
  
  markDirty(bounds) {
    this.dirtyRects.addFromBounds(bounds, 5)
  }
  
  render() {
    const dirtyRect = this.dirtyRects.merge()
    
    if (!dirtyRect) return
    
    this.ctx.save()
    this.ctx.beginPath()
    this.ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)
    this.ctx.clip()
    
    this.ctx.drawImage(
      this.backgroundCanvas,
      dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height,
      dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height
    )
    
    this.objects.forEach(obj => {
      if (this.intersects(obj, dirtyRect)) {
        obj.render(this.ctx)
      }
    })
    
    this.ctx.restore()
    this.dirtyRects.clear()
  }
  
  intersects(obj, rect) {
    const bounds = obj.getBounds()
    return bounds.x < rect.x + rect.width &&
           bounds.x + bounds.width > rect.x &&
           bounds.y < rect.y + rect.height &&
           bounds.y + bounds.height > rect.y
  }
  
  resize(width, height) {
    this.canvas.width = width
    this.canvas.height = height
    this.backgroundCanvas.width = width
    this.backgroundCanvas.height = height
    this.backgroundValid = false
    this.renderBackground()
  }
}

实战示例

动画脏矩形优化

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Canvas 脏矩形优化 - 动画示例</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      background: #1a1a2e;
      font-family: system-ui, sans-serif;
    }
    
    .container {
      max-width: 900px;
      margin: 0 auto;
    }
    
    h1 {
      color: #eee;
      text-align: center;
    }
    
    .canvas-container {
      display: flex;
      gap: 20px;
      flex-wrap: wrap;
      justify-content: center;
    }
    
    .canvas-wrapper {
      background: #16213e;
      border-radius: 8px;
      padding: 10px;
    }
    
    .canvas-wrapper h3 {
      color: #eee;
      margin: 0 0 10px;
      text-align: center;
    }
    
    canvas {
      display: block;
      background: #0f0f23;
      border-radius: 4px;
    }
    
    .stats {
      color: #00ff00;
      font-family: monospace;
      font-size: 12px;
      margin-top: 10px;
      text-align: center;
    }
    
    .controls {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin-top: 20px;
    }
    
    button {
      padding: 10px 20px;
      background: #e94560;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    
    button:hover {
      background: #ff6b6b;
    }
    
    .show-dirty {
      outline: 2px solid #00ff00;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>脏矩形优化对比</h1>
    
    <div class="canvas-container">
      <div class="canvas-wrapper">
        <h3>全屏重绘</h3>
        <canvas id="fullCanvas" width="400" height="300"></canvas>
        <div class="stats" id="fullStats">FPS: 0 | 像素: 0</div>
      </div>
      
      <div class="canvas-wrapper">
        <h3>脏矩形重绘</h3>
        <canvas id="dirtyCanvas" width="400" height="300"></canvas>
        <div class="stats" id="dirtyStats">FPS: 0 | 像素: 0</div>
      </div>
    </div>
    
    <div class="controls">
      <button onclick="toggleAnimation()">暂停/继续</button>
      <button onclick="toggleDirtyRects()">显示脏矩形</button>
      <button onclick="changeCount(10)">10对象</button>
      <button onclick="changeCount(50)">50对象</button>
      <button onclick="changeCount(100)">100对象</button>
    </div>
  </div>
  
  <script>
    class AnimatedObject {
      constructor(canvasWidth, canvasHeight) {
        this.canvasWidth = canvasWidth
        this.canvasHeight = canvasHeight
        this.x = Math.random() * (canvasWidth - 40)
        this.y = Math.random() * (canvasHeight - 40)
        this.width = 30 + Math.random() * 20
        this.height = 30 + Math.random() * 20
        this.vx = (Math.random() - 0.5) * 4
        this.vy = (Math.random() - 0.5) * 4
        this.color = `hsl(${Math.random() * 360}, 70%, 60%)`
        this.prevX = this.x
        this.prevY = this.y
      }
      
      update() {
        this.prevX = this.x
        this.prevY = this.y
        
        this.x += this.vx
        this.y += this.vy
        
        if (this.x <= 0 || this.x + this.width >= this.canvasWidth) {
          this.vx *= -1
          this.x = Math.max(0, Math.min(this.canvasWidth - this.width, this.x))
        }
        if (this.y <= 0 || this.y + this.height >= this.canvasHeight) {
          this.vy *= -1
          this.y = Math.max(0, Math.min(this.canvasHeight - this.height, this.y))
        }
      }
      
      getBounds() {
        return { x: this.x, y: this.y, width: this.width, height: this.height }
      }
      
      getPreviousBounds() {
        return { x: this.prevX, y: this.prevY, width: this.width, height: this.height }
      }
      
      render(ctx) {
        ctx.fillStyle = this.color
        ctx.fillRect(this.x, this.y, this.width, this.height)
      }
    }
    
    class FullRenderer {
      constructor(canvas) {
        this.canvas = canvas
        this.ctx = canvas.getContext('2d')
        this.objects = []
        this.pixelsProcessed = 0
        this.fps = 0
        this.frameCount = 0
        this.lastTime = performance.now()
      }
      
      init(count) {
        this.objects = []
        for (let i = 0; i < count; i++) {
          this.objects.push(new AnimatedObject(this.canvas.width, this.canvas.height))
        }
      }
      
      render() {
        this.ctx.fillStyle = '#0f0f23'
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
        
        this.objects.forEach(obj => {
          obj.update()
          obj.render(this.ctx)
        })
        
        this.pixelsProcessed = this.canvas.width * this.canvas.height
        this.updateFPS()
      }
      
      updateFPS() {
        this.frameCount++
        const now = performance.now()
        if (now - this.lastTime >= 1000) {
          this.fps = this.frameCount
          this.frameCount = 0
          this.lastTime = now
        }
      }
    }
    
    class DirtyRenderer {
      constructor(canvas) {
        this.canvas = canvas
        this.ctx = canvas.getContext('2d')
        this.objects = []
        this.dirtyRects = []
        this.pixelsProcessed = 0
        this.fps = 0
        this.frameCount = 0
        this.lastTime = performance.now()
        this.showDirtyRects = false
      }
      
      init(count) {
        this.objects = []
        for (let i = 0; i < count; i++) {
          this.objects.push(new AnimatedObject(this.canvas.width, this.canvas.height))
        }
        
        this.ctx.fillStyle = '#0f0f23'
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
      }
      
      render() {
        this.dirtyRects = []
        
        this.objects.forEach(obj => {
          const prevBounds = obj.getPreviousBounds()
          const currBounds = obj.getBounds()
          
          this.addDirtyRect(prevBounds)
          this.addDirtyRect(currBounds)
          
          obj.update()
        })
        
        const mergedRect = this.mergeDirtyRects()
        
        if (mergedRect) {
          this.ctx.save()
          this.ctx.beginPath()
          this.ctx.rect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height)
          this.ctx.clip()
          
          this.ctx.fillStyle = '#0f0f23'
          this.ctx.fillRect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height)
          
          this.objects.forEach(obj => {
            if (this.intersects(obj.getBounds(), mergedRect)) {
              obj.render(this.ctx)
            }
          })
          
          if (this.showDirtyRects) {
            this.ctx.strokeStyle = '#00ff00'
            this.ctx.lineWidth = 2
            this.ctx.strokeRect(mergedRect.x, mergedRect.y, mergedRect.width, mergedRect.height)
          }
          
          this.ctx.restore()
          
          this.pixelsProcessed = mergedRect.width * mergedRect.height
        }
        
        this.updateFPS()
      }
      
      addDirtyRect(bounds) {
        this.dirtyRects.push({
          x: bounds.x - 2,
          y: bounds.y - 2,
          width: bounds.width + 4,
          height: bounds.height + 4
        })
      }
      
      mergeDirtyRects() {
        if (this.dirtyRects.length === 0) return null
        
        let minX = Infinity, minY = Infinity
        let maxX = -Infinity, maxY = -Infinity
        
        this.dirtyRects.forEach(r => {
          minX = Math.min(minX, r.x)
          minY = Math.min(minY, r.y)
          maxX = Math.max(maxX, r.x + r.width)
          maxY = Math.max(maxY, r.y + r.height)
        })
        
        return {
          x: Math.max(0, minX),
          y: Math.max(0, minY),
          width: Math.min(this.canvas.width - minX, maxX - minX),
          height: Math.min(this.canvas.height - minY, maxY - minY)
        }
      }
      
      intersects(bounds, rect) {
        return bounds.x < rect.x + rect.width &&
               bounds.x + bounds.width > rect.x &&
               bounds.y < rect.y + rect.height &&
               bounds.y + bounds.height > rect.y
      }
      
      updateFPS() {
        this.frameCount++
        const now = performance.now()
        if (now - this.lastTime >= 1000) {
          this.fps = this.frameCount
          this.frameCount = 0
          this.lastTime = now
        }
      }
      
      toggleShowDirtyRects() {
        this.showDirtyRects = !this.showDirtyRects
      }
    }
    
    const fullCanvas = document.getElementById('fullCanvas')
    const dirtyCanvas = document.getElementById('dirtyCanvas')
    const fullStats = document.getElementById('fullStats')
    const dirtyStats = document.getElementById('dirtyStats')
    
    const fullRenderer = new FullRenderer(fullCanvas)
    const dirtyRenderer = new DirtyRenderer(dirtyCanvas)
    
    let objectCount = 50
    let isRunning = true
    
    function init() {
      fullRenderer.init(objectCount)
      dirtyRenderer.init(objectCount)
    }
    
    function animate() {
      if (!isRunning) return
      
      fullRenderer.render()
      dirtyRenderer.render()
      
      fullStats.textContent = `FPS: ${fullRenderer.fps} | 像素: ${fullRenderer.pixelsProcessed.toLocaleString()}`
      dirtyStats.textContent = `FPS: ${dirtyRenderer.fps} | 像素: ${dirtyRenderer.pixelsProcessed.toLocaleString()}`
      
      requestAnimationFrame(animate)
    }
    
    function toggleAnimation() {
      isRunning = !isRunning
      if (isRunning) animate()
    }
    
    function toggleDirtyRects() {
      dirtyRenderer.toggleShowDirtyRects()
      dirtyCanvas.classList.toggle('show-dirty')
    }
    
    function changeCount(count) {
      objectCount = count
      init()
    }
    
    init()
    animate()
  </script>
</body>
</html>

最佳实践

脏矩形使用场景

const dirtyRectUseCases = {
  recommended: [
    {
      scenario: '少量移动对象',
      example: '游戏角色、UI元素',
      benefit: '大幅减少重绘区域'
    },
    {
      scenario: '局部更新',
      example: '数据点变化、进度条',
      benefit: '只更新变化部分'
    },
    {
      scenario: '静态背景',
      example: '地图、面板',
      benefit: '背景无需重绘'
    }
  ],
  
  notRecommended: [
    {
      scenario: '全屏动态效果',
      example: '全屏粒子、波浪',
      reason: '脏区域接近全屏'
    },
    {
      scenario: '大量小对象',
      example: '数千个小粒子',
      reason: '合并后区域过大'
    },
    {
      scenario: '频繁全局变化',
      example: '背景动画',
      reason: '无法有效减少重绘'
    }
  ]
}

性能监控

class DirtyRectMonitor {
  constructor() {
    this.stats = {
      totalFrames: 0,
      dirtyFrames: 0,
      totalDirtyPixels: 0,
      totalCanvasPixels: 0,
      dirtyRectsPerFrame: []
    }
  }
  
  recordFrame(canvasWidth, canvasHeight, dirtyRects) {
    this.stats.totalFrames++
    this.stats.totalCanvasPixels = canvasWidth * canvasHeight
    
    if (dirtyRects && dirtyRects.length > 0) {
      this.stats.dirtyFrames++
      
      let framePixels = 0
      dirtyRects.forEach(r => {
        framePixels += r.width * r.height
      })
      
      this.stats.totalDirtyPixels += framePixels
      this.stats.dirtyRectsPerFrame.push(dirtyRects.length)
    }
  }
  
  getReport() {
    const avgDirtyPixels = this.stats.totalFrames > 0
      ? this.stats.totalDirtyPixels / this.stats.totalFrames
      : 0
    
    const avgDirtyRatio = this.stats.totalCanvasPixels > 0
      ? avgDirtyPixels / this.stats.totalCanvasPixels
      : 0
    
    const avgDirtyRects = this.stats.dirtyRectsPerFrame.length > 0
      ? this.stats.dirtyRectsPerFrame.reduce((a, b) => a + b, 0) / this.stats.dirtyRectsPerFrame.length
      : 0
    
    return {
      totalFrames: this.stats.totalFrames,
      dirtyFrames: this.stats.dirtyFrames,
      avgDirtyPixels: Math.round(avgDirtyPixels),
      avgDirtyRatio: (avgDirtyRatio * 100).toFixed(2) + '%',
      avgDirtyRects: avgDirtyRects.toFixed(2),
      efficiency: ((1 - avgDirtyRatio) * 100).toFixed(2) + '%'
    }
  }
  
  reset() {
    this.stats = {
      totalFrames: 0,
      dirtyFrames: 0,
      totalDirtyPixels: 0,
      totalCanvasPixels: 0,
      dirtyRectsPerFrame: []
    }
  }
}

总结

脏矩形技术是Canvas性能优化的重要手段:

  1. 减少重绘区域:只更新发生变化的区域
  2. 对象追踪:自动检测对象位置变化
  3. 背景缓存:预渲染静态背景
  4. 多矩形优化:智能合并脏矩形
  5. 性能监控:追踪优化效果

合理运用脏矩形技术,可以显著减少像素处理量,提升Canvas应用的渲染性能。