viewport 必须包含 initial-scale=1.0 和 user-scalable=no 才能正确适配;canvas 的 width/height 与 style.width/style.height 必须分离设置并按 dpr 动态计算;禁用 transform: scale();需监听 dpr 变化而非仅 resize。

viewport 里 initial-scale=1.0 是前提,不是可选项
很多 canvas 模糊问题,其实发生在页面刚加载时就错位了。根本原因不是 canvas 本身,而是 viewport 元标签没写对。iOS Safari 和部分安卓浏览器在没有 initial-scale=1.0 时,会默认用桌面视口(比如 980px)渲染,导致 canvas.clientWidth 拿到的是错误尺寸,后续所有 DPR 计算全偏移。
必须写成这一行,且 user-scalable=no 不能省——双击缩放会触发 layout 重排,动画帧直接跳变:
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
注意:width=device-width 单独存在 ≠ 适配,它只是告诉浏览器“CSS 像素宽度 = 设备物理宽度 ÷ 缩放值”,真正锁定像素对齐的是 initial-scale=1.0。
canvas 的 width/height 和 style.width/style.height 必须分离设置
这是最常踩的坑:只改 CSS 尺寸,不改 DOM 属性,等于让浏览器强行拉伸低分辨率画布,必然模糊。
-
canvas.width和canvas.height是缓冲区大小,决定实际绘制的物理像素数 -
canvas.style.width和canvas.style.height是显示尺寸,控制它在页面上占多少 CSS 像素 - 两者数值关系必须是:
canvas.width === canvas.clientWidth * dpr,canvas.style.width === canvas.clientWidth + "px"
推荐用 getBoundingClientRect() 获取当前显示尺寸,它不受 transform、缩放或父容器 overflow 影响:
const rect = canvas.getBoundingClientRect();<br>canvas.width = Math.round(rect.width * dpr);<br>canvas.height = Math.round(rect.height * dpr);<br>canvas.style.width = rect.width + "px";<br>canvas.style.height = rect.height + "px";
禁止对 canvas 元素使用 transform: scale()
一旦给 canvas 加了 transform: scale(0.5) 或类似 CSS 缩放,就会和 ctx.scale(dpr, dpr) 叠加,造成双重插值——浏览器先按 CSS 缩放重采样一次,再由 Canvas 渲染器做第二次缩放,像素彻底失焦。
正确做法只有两个:
- 缩放逻辑全部交给
ctx.scale(),绘图坐标仍按 CSS 像素书写(如fillRect(10, 10, 20, 20)) - 如果真要响应式缩放整个 canvas 区域,用外层容器包裹,通过 flex / grid 控制其布局尺寸,canvas 自身保持 1:1 显示
特别注意:iOS Safari 对 transform: scale() 的处理更激进,即使 scale=1 也可能触发不必要的合成层重绘,导致动画掉帧。
监听设备像素比变化比监听 resize 更关键
DPR 不是静态值。iOS 地址栏收起、分屏模式切换、系统级缩放设置变更、甚至某些安卓厂商 UI 切换,都可能让 window.devicePixelRatio 突然改变。只靠 resize 事件完全捕获不到。
要用媒体查询监听分辨率变化:
const mediaQuery = window.matchMedia(`screen and (min-resolution: ${dpr}dppx)`);<br>mediaQuery.addEventListener("change", () => {<br> // 重新计算 canvas 尺寸、清空、ctx.scale、重绘<br> setupCanvas(canvas);<br>});
加上简单防抖(如 200ms),避免连续触发卡顿。复杂场景建议用离屏 canvas 预渲染,减少主线程重绘压力。
真正难的不是第一次适配,而是 DPR 动态变化后能否无缝更新——很多线上 bug 都藏在这里,尤其在横竖屏切换或 iOS Safari 地址栏收放瞬间。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











