适用场景:H5、Safari、WKWebView、小程序 WebKit、海报长图导出、网页截图、图片合成
关键词:Canvas 静默失效、WebKit 硬限制、Jetsam OOM、分片瓦片绘制、内存泄漏
一、现象与根因
很多前端在做海报导出、长截图、大图合成时,会遇到诡异 Bug:
- 代码无报错,
toDataURL/toBlob返回空白图片 - 绘制完成画布整体变白,控制台没有异常抛出
- 导出大图后页面直接刷新、白屏,用户体验极差
根本原因:iOS WebKit 存在 Canvas 硬上限
经典限制:单张画布最大像素 4096 × 4096 = 16,777,216 px,RGBA 每像素占 4 字节,满画布占用约 64MB 显存 / 内存。
新高端机型理论可放宽至 8192×8192,但内存紧张时依然会静默失败,没有任何 JS 异常提示。
重要提醒:
canvas.width / height(物理像素)≠ CSS 样式宽高,超限判断必须使用物理像素;- OffscreenCanvas、WebGL 纹理同样受该限制,无法绕过 WebKit 限制;
- 多画布并发创建会触发 WebContent 进程 OOM,系统通过 Jetsam 机制直接杀掉进程,页面刷新白屏。
二、完整防护策略
整套防护分为 5 个核心环节:前置尺寸校验、DPR 动态适配、分片瓦片绘制、手动内存释放、超限降级兜底。
1. 前置安全校验(基础拦截)
不建议直接写死 4096,生产环境推荐保守阈值 3840px,预留安全余量,避免部分内存较小的老旧机型翻车。
js
运行
// 安全阈值
const SAFE_MAX = 3840;
const SAFE_AREA = SAFE_MAX * SAFE_MAX;
/**
* 校验画布尺寸是否安全
* @param {number} width 画布物理宽
* @param {number} height 画布物理高
* @returns boolean
*/
function isCanvasSafe(width, height) {
const area = width * height;
return width <= SAFE_MAX && height <= SAFE_MAX && area <= SAFE_AREA;
}
进阶方案:运行时探测设备真实上限,不要依赖 UA 判断机型,不同内存版本上限差异很大。
生产推荐开源库:canvas-size,专门探测当前浏览器支持的最大画布尺寸,支持 OffscreenCanvasGitHub。
2. 动态适配 DPR,避免 Retina 屏意外超限
高 DPR 手机(devicePixelRatio=2/3),视觉尺寸不大,但物理画布会被成倍放大,极易触发上限。
解决方案:保持 CSS 视觉尺寸不变,超限后动态降低 DPR,缩小物理画布分辨率。
js
运行
/**
* 根据CSS尺寸,返回安全的物理画布尺寸
* @param {number} cssW 样式宽度
* @param {number} cssH 样式高度
* @returns { physW, physH } 画布物理宽高
*/
function getSafeCanvasSize(cssW, cssH) {
const dpr = window.devicePixelRatio || 1;
let physW = cssW * dpr;
let physH = cssH * dpr;
if (!isCanvasSafe(physW, physH)) {
// 计算缩放比例,预留5%余量
const scale = Math.sqrt(SAFE_AREA / (physW * physH)) * 0.95;
const newDpr = Math.max(1, dpr * scale);
physW = Math.round(cssW * newDpr);
physH = Math.round(cssH * newDpr);
}
return { physW, physH };
}
3. 超大长图:分片瓦片绘制(核心方案)
当整体图片物理尺寸远超安全上限(超长海报、长页面截图),禁止创建一张超大画布。
方案思路:
- 将整张图纵向 / 横向切分为瓦片,单块瓦片尺寸控制在安全范围内;
- 循环逐块绘制,每块绘制完成导出 Blob /base64;
- 绘制完成立即销毁临时画布,释放内存;
- 所有瓦片导出完成后,使用一张安全画布拼接完整大图,或交由后端服务合并图片。
开发规范:禁止全局长期保存大量临时 canvas、ImageData、base64 字符串,极易造成内存堆积。
4. iOS 必做:手动释放画布内存
iOS JavaScript GC 回收时机不可控,画布像素缓冲区不会自动及时释放,大量临时画布叠加会快速 OOM。
绘制完成后强制销毁画布:
js
运行
/**
* 销毁画布,释放像素缓冲区内存
* @param {HTMLCanvasElement | OffscreenCanvas} canvas
*/
function destroyCanvas(canvas) {
if (!canvas) return;
const ctx = canvas.getContext("2d");
ctx?.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = 0;
canvas.height = 0;
canvas.remove?.();
}
5. 超限兜底降级方案
当尺寸无论如何压缩都无法放入单张安全画布时,提供多级降级:
- 降低整体导出分辨率,适当牺牲画质;
- 前端分片输出多张切片,后端服务合并完整大图;
- 超长文档优先使用 SVG / PDF 导出,替代全量 Canvas 绘制;
- UI 交互提示用户:图片尺寸过大,建议缩小素材后重新生成。
三、原生 iOS(WKWebView / Swift CoreGraphics)补充防护
如果是 App 内嵌 H5(WKWebView),除前端 JS 防护,原生层也需要兜底:
- 监听
webViewWebContentProcessDidTerminateWeb 进程崩溃回调,做页面恢复; - 监听内存警告
didReceiveMemoryWarning,通知 JS 销毁全部临时画布、释放图片资源; - 控制页面并发画布数量,串行绘图,禁止同时创建多张大画布。
原生 CoreGraphics / UIGraphics 绘制同样存在内存上限:
- 预估内存占用:
字节 = width * height * 4,创建前预判可用内存; - 巨图绘制使用 mmap 内存映射,像素写入临时磁盘文件,不占用进程 RAM;
- 使用完毕及时释放 CGContext、CGImage 对象,避免 C 层内存泄漏。
四、高频踩坑清单
- ❌ 混淆 CSS 样式尺寸与 canvas 物理宽高,只用样式判断是否超限;
- ❌ 认为 OffscreenCanvas / WebGL 纹理可以绕过 4096 尺寸限制;
- ❌ 大量临时画布不手动销毁,依赖 JS GC 自动回收;
- ❌ 直接将相机原图(几 k~ 上万像素原图)丢入 canvas,未提前下采样;
- ❌ 使用 UA 判断设备上限,没有运行时探测真实画布最大支持尺寸;
- ❌ 多张大画布并发绘制,短时间耗尽 WebKit 进程内存,触发 Jetsam 杀进程。
五、总结
iOS Canvas 超限问题最大难点是静默失败,无报错,线上很难复现定位。
完整防护链路:运行时探测上限 → 前置尺寸校验 → DPR 动态降采样 → 超大图分片瓦片绘制 → 绘制后手动释放内存 → 超限降级兜底。
海报导出、长截图等重 Canvas 场景,建议上线前在老旧小内存 iPhone 真机充分测试,避免线上大面积白屏崩溃。
如果你需要,我可以补充一份完整可上线的【分片瓦片绘制 + 图片合并】生产 JS Demo,包含 blob 拼接、进度回调、异常捕获。
扩展阅读
- canvas-size 官方仓库:https://github.com/jhildenbiddle/canvas-size
- WebKit Bugzilla:Canvas 超大尺寸硬件加速失效问题