关键在于避免重复缩放和冗余绘制:统一用逻辑像素坐标、静态内容离屏缓存、图像预缩放与解码、禁用插值、节流渲染。

避免高清屏 Canvas 放大时的双倍渲染开销,关键不是“少画”,而是“不重复画”——尤其要防止因 dpr 处理不当,导致同一帧内既缩放坐标系又手动放大尺寸、既重绘背景又反复 clear 整画布、既用主 Canvas 渲染又误启离屏 Canvas 的冗余路径。
只缩放一次:物理尺寸与坐标系对齐,不叠加缩放
常见错误是:先设置 canvas.width = width * dpr 和 canvas.height = height * dpr,再调用 ctx.scale(dpr, dpr),最后在绘制时又把所有坐标再乘一遍 dpr(比如写 ctx.fillRect(x*dpr, y*dpr, w*dpr, h*dpr))。这等于让浏览器做了三重缩放:Canvas 缓冲区放大 × 坐标系缩放 × 手动坐标放大 → 实际渲染像素翻了 dpr² 倍,GPU 填充量暴增。
正确做法是:
- 设置 canvas 内在尺寸为
逻辑宽 × dpr/逻辑高 × dpr - 立即执行
ctx.scale(dpr, dpr),此后所有绘图坐标按逻辑像素书写(如fillRect(10, 10, 50, 30)) - 不再对坐标、宽高做任何额外乘法;
drawImage传入的源/目标区域也统一用逻辑尺寸
分层复用:静态内容进离屏 Canvas,动态区域才重绘
若整个画布都随 dpr 放大,但其中 80% 是不变的网格、坐标轴、底纹或 UI 背景,每帧仍全量重绘,就白白消耗 CPU 路径重建 + GPU 像素填充。这不是模糊问题,而是无效渲染开销。
应拆分为:
- 一个离屏 Canvas(
OffscreenCanvas或 DOM 隐藏<canvas></canvas>)专绘静态层,创建后只画一次 - 主 Canvas 只负责 drawImage 合成静态层 + 绘制动态对象(如移动点、拖拽框、实时曲线)
- 动态区域若局部变化,用
clearRect()精确擦除对应逻辑区域,而非clearRect(0,0,canvas.width,canvas.height)
资源加载与绘制解耦:图像缩放不发生在主渲染帧中
高清屏下直接 drawImage(largeImg, ...) 容易触发浏览器内部双线性重采样 + 缓冲区拉伸,尤其当 largeImg 是 4K 原图时,会隐式产生高开销中间渲染步骤。
优化方式:
- 提前按显示逻辑尺寸 +
dpr计算出目标绘制区域,再从原图裁剪并缩放(用drawImage(img, sx,sy,sw,sh, dx,dy,dw,dh)精确控制) - 对固定图标/素材,提供
@2x版本并用ImageBitmap预解码,避免每次绘制都触发解码和缩放 - 禁用插值:设置
ctx.imageSmoothingEnabled = false,尤其适用于像素风、矢量图、UI 图标等无需抗锯齿的场景
节流与合并:避免高频操作放大 DPR 副作用
触摸拖拽、鼠标 hover 等交互若每毫秒都触发 clear + redraw,在 dpr=2 下实际等于每秒提交 120 帧 × 每帧 4× 像素填充量,极易卡顿。
应对策略:
- 用
requestAnimationFrame节流,确保最多 60 FPS;对非关键动画可主动限帧(如 30 FPS) - 清除多个相邻区域时,先合并为最小包围矩形,单次
clearRect()替代多次调用 - 坐标换算仅用于定位(如计算裁剪框位置),真正传给
drawImage的sx/sy仍基于原图分辨率,与 DPR 无关
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











