不能直接在matchmedia回调中立即重绘canvas,应使用requestanimationframe节流并防抖,仅更新主题状态变量,由主动画循环统一判断重绘,避免高频触发导致撕裂或延迟。

matchMedia 回调里直接重绘 Canvas 会卡顿
很多人以为 matchMedia 的回调一触发就立刻 ctx.clearRect() + draw() 就完事,结果滚动或系统主题切换瞬间 Canvas 出现明显撕裂或延迟。根本原因是回调可能高频触发(比如 macOS 的深色模式过渡动画期间),而 Canvas 重绘本身有开销,没节流就会压垮渲染帧率。
实操建议:
- 用
requestAnimationFrame包裹重绘逻辑,确保只在下一帧执行,避免连环重绘 - 加一个简单防抖:记录上一次执行时间,间隔
16ms内的重复回调直接忽略 - 别在回调里做颜色解析、资源加载等耗时操作——提前把深/浅色主题的配色对象存好,比如
themeColors = { bg: '#ffffff', text: '#000000' }和darkThemeColors
监听 prefers-color-scheme 变化时 Canvas 渲染上下文丢失
典型错误是把 canvas.getContext('2d') 写在 matchMedia 回调里,导致每次主题切换都新建 ctx,旧绘制状态清空,甚至引发 SecurityError(尤其 canvas 被跨域图片填充后)。
实操建议:
-
getContext必须在页面初始化时一次性获取并缓存,比如const ctx = canvas.getContext('2d')放在全局或模块顶层 - 回调里只改
ctx.fillStyle、ctx.strokeStyle等属性,然后调用重绘函数 - 如果用了
canvas.toDataURL()或导出功能,注意主题变更后导出图仍按旧色——需在重绘完成后才允许导出
Canvas 动画循环和 matchMedia 回调冲突
如果你的 Canvas 正在跑 requestAnimationFrame(animate) 动画(比如粒子、波形),而 matchMedia 回调又触发重绘,两者没同步就容易出现颜色闪动、图层错位。
实操建议:
- 把主题色变量设为响应式状态,比如
let currentTheme = 'light',回调里只更新这个值,不碰 Canvas - 在
animate()主循环里判断if (currentTheme !== lastRenderedTheme),再执行完整重绘 - 避免在回调里调用
ctx.save()/ctx.restore()—— 这些该由动画主逻辑统一管理堆栈
移动端 Safari 下 prefers-color-scheme 不触发回调
iOS 13+ 支持 prefers-color-scheme,但 Safari 有个隐藏行为:当用户从控制中心快速开关深色模式,首次切换可能不触发 addEventListener('change'),尤其是页面刚加载完还没完成首次渲染时。
实操建议:
- 初始化时立即读一次
mediaQuery.matches并应用主题,别只依赖回调 - 加一个兜底轮询(仅 iOS):
setTimeout(() => checkTheme(), 500),检查matchMedia('(prefers-color-scheme: dark)').matches是否与当前状态一致 - 不要用
matchMedia('screen')或其他伪媒体查询替代——只有prefers-color-scheme是标准且可监听的











