canvas-img引发的内存问题

发表于 JS 分类,标签:

版本信息

作者时间主要变更内容链接
吴惟刚2023年10月10日http://wuweigang.com/?id=395

问题描述

页面滚动按需加载图片,越往下滚动页面图片越来越多(图片采用canvas渲染), 机器为8GB内存机器,在chrome下大约3000张图,浏览器崩溃, 每张图平均1MB

系统问题排查定位

如果不加载图片, 不渲染canvas, 浏览器内存不会增长, 基本问题就出现在这里

问题定位追踪测试

环境信息

  • window10 chrome 117.0.5938.134 64

  • window7 chrome 75.0.3770.38 32

  • 下面测试简称 window10和 window7

  • 页签自己的进程为渲染进程, gpu进程各页签共享进程, chrome为多进程架构

测试如下

测试1

  1. 用js循环生成1000个canvas, 存储到某个变量中, canvas 宽高大小为512乘512,查阅知识一个,一个像素的canvas为4byte, 因此512乘512乘4=1MB

    • 通过getImageData()可以获取canvas存的信息,像素信息使用 Uint8 来存储的,数组长度为 4, Uint8 占用内存为 1 个字节, 因此一共是 4 个字节,因此一个像素的canvas为4byte

  • window7
    页签内存增高,内存占用 1065200 kb

  • window10

    致GPU 内存增高, GPU进程 1161568 k , 页签内存无变化,占用 44228k kb,

测试1结论

结论, 1000 canvas会导致内存升高1000 MB , 且不会销毁! window10 导致GPU 内存增高, window7导致页签内存增高。

测试2

  1. 用js循环生成100个 new Image(), 图片的大小为3.5MB (window7 500个)

  • window7

在图片加载过程中,内存一度上升到3000MB 以上, 加载完,稳定后保持在 1773928 k

  • 将new Image() 全部存储到某个变量中, 内存不会下降销毁

  • 将new Image() 加载完后,再将变量设置成null, 内存会下降销毁

  • 将new Image() 加载完后,再将变量设置成null, 并且每个new Image()对象,创建一个 img标签,最后统一放到html中显示, 内存不会下载销毁

  • 将new Image() 加载完后,再将变量设置成null, 并且仅仅创建一个 img标签,最后放到html中显示, 内存会下降销毁, 仅仅保持img中显示的那个内存

  • window10
    在图片加载过程中,内存一度上升到3511932k 以上, 加载完,稳定后保持在 1773928 k

  • 将new Image() 全部存储到某个变量中, 内存会下降销毁, 内训会下降到 45684k

  • 将new Image() 加载完后,再将变量设置成null, 内存会下降销毁

  • 将new Image() 加载完后,再将变量设置成null, 并且每个new Image()对象,创建一个 img标签,最后统一放到html中显示, 内存会下载销毁

  • 将new Image() 加载完后,再将变量设置成null, 并且仅仅创建一个 img标签,最后放到html中显示, 内存会下降销毁

测试2结论

根据以上测试,猜测window10上的chrome做了改良,img 用完后会自动销毁,dom元素不会占用太多内存, window7 下的chrome无法销毁,除非该图片不被引用,否则内存永远存在!

测试3

  1. 用js循环生成1000个 canvas, 并且请求图片, 图片的大小为3.5MB, 将其渲染到canvas上 (window7 500个)

  • window7
    在图片加载过程中,内存一度上升到3000MB 以上, 加载完,稳定后保持在 1998728 k

  • 将canvas 全部存储到某个变量中, 内存不会下降销毁

  • 将canvas 全部销毁,new Image() 不销毁, 内存不会下降销毁

  • 将canvas 全部销毁,new Image() 全部销毁, 内存会下降销毁

  • window10
    在图片加载过程中,内存一度上升到3000MB 以上, 加载完,稳定后保持在 1998728 k

  • 将canvas 全部存储到某个变量中, 内存不会下降销毁,保留canvas占用的内存。
    渲染进程非激活情况下,稳定在1076260k, 激活情况下 39120k, GPU进程会优先接管canvas渲染进程 1342064k

  • 将canvas 全部销毁,new Image() 不销毁, 内存不会下降销毁,保留canvas占用的内存

  • 将canvas 全部销毁,new Image() 全部销毁, 内存会下降销毁

测试3结论

  • window7下,canvas渲染图片内存会更高,且不会回收, 因此跟canvas和图片两者相关

  • window10, 各个渲染进程的canvas使用到的内存会在页签激活时,被gpu进程取代,内存转移到GPU内存,占用内存情况跟canvas相关,跟图片无关

基于图片和canvas降低内存的优化建议

  1. 如果是使用 new Image() 加载图片,那么变量中不缓存 new Image() 对象

  2. 如果是使用 img加载图片,那么应该尽量使用虚拟列表,减少页面的img标签

  3. 如果使用canvas循环图片,不要在js变量中缓存canvas, 尽量使用虚拟dom,减少页面的canvas数量

  4. 图片加载过程中window7会持续走高,在此过程并不会降低内存, 因此应该尽量避免持续请求图片,可以变成间隔请求



demo

大家可以自己复杂代码进行测试,图片自己在网上下载,把 src 修改为自己的图片即可

demo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8"/>
   <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/>
   <title>canvas limit</title>
</head>
<body>
<div>
   <span>内存单位: MB</span>
   <input type="number" id="jsNumber"/>
</div>
<div>
   <button id="jsCreate">创建</button>
</div>
<script>
   // 放进该全局变量,防止GC
   let queue = [];
   let index = 0;
   const documentFrame = document.createDocumentFragment();
   // 创建 对象
   const createObject = (count) => {
       const size = 512;
       const canvas = document.createElement('canvas');
       canvas.width = size;
       canvas.height = size;
       const context = canvas.getContext('2d');
       context.fillRect(0, 0, size, size);
       index++;
       if (index === count) {
           const span = document.createElement('span');
           span.innerHTML = '完成' + count;
           document.body.appendChild(span);
           document.body.appendChild(documentFrame);
       }
       return canvas;
   };

   // 循环创建对象
   const loopCreateObject = (n) => {
       for (let i = 0; i < n; i++) {
           queue.push(createObject(n));
       }
   };
   const input = document.querySelector('#jsNumber');
   const button = document.querySelector('#jsCreate');
   button.addEventListener('click', (event) => {
       event.preventDefault();
       const number = input.value;
       if (!Number.isNaN(Number(number))) {
           queue = [];
           loopCreateObject(Number(number));
           console.log(`创建${number}MB canvas成功`);
       }
   });
</script>
</body>
</html>

demo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8"/>
   <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/>
   <title>img limit</title>
</head>
<body>
<div>
   <span>内存单位: MB</span>
   <input type="number" id="jsNumber"/>
</div>
<div>
   <button id="jsCreate">创建</button>
</div>
<script>
   // 放进该全局变量,防止GC
   let queue = [];
   let index = 0;
   const documentFrame = document.createDocumentFragment();
   // 创建 对象
   const createObject = (count) => {
       index++;
       let img = new Image();
       img.index = index;
       img.onload = () => {
           if (img.index === count) {
               const span = document.createElement('span');
               span.innerHTML = '完成' + count;
               document.body.appendChild(span);
               document.body.appendChild(documentFrame);
           }
           // const  targetImg = document.createElement('img');
           // targetImg.src = 'img/img3024-4032.jpg?index=' + img.index;
           // documentFrame.appendChild(targetImg);
           // img = null;
       };
       // 加载图片出错的处理
       img.onerror = (error) => {
       };
       // 84KB
       // img.src = 'img/img512-min.jpg?index=' + index;
       // 3.35MB
       img.src = 'img/img3024-4032.jpg?index=' + index;
       // img.src = 'img/img512-max.jpg?index=' + index;
       // 240kb
       // img.src = 'img/img512-zmax.jpg?index=' + index;
       return img;
   };

   // 循环创建对象
   const loopCreateObject = (n) => {
       for (let i = 0; i < n; i++) {
           queue.push(createObject(n));
       }
   };

   const input = document.querySelector('#jsNumber');
   const button = document.querySelector('#jsCreate');

   button.addEventListener('click', (event) => {
       event.preventDefault();
       const number = input.value;
       if (!Number.isNaN(Number(number))) {
           queue = [];
           loopCreateObject(Number(number));
           console.log(`创建${number}MB canvas成功`);
       }
   });
</script>
</body>
</html>

demo3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!DOCTYPE html>
<html>
<head>
   <meta charset="UTF-8"/>
   <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"/>
   <title>img-canvas limit</title>
</head>
<body>
<div>
   <span>内存单位: MB</span>
   <input type="number" id="jsNumber"/>
</div>
<div>
   <button id="jsCreate">创建</button>
</div>
<script>
   // 放进该全局变量,防止GC
   let queue = [];
   let index = 0;
   const documentFrame = document.createDocumentFragment();
   // 创建 对象
   const createObject = (count) => {
       const size = 512;
       let canvas = document.createElement('canvas');
       canvas.width = size;
       canvas.height = size;
       let context = canvas.getContext('2d');
       index++;
       let img = new Image();
       img.index = index;
       img.onload = () => {
           context.drawImage(img, 0, 0, size, size);
           if (img.index === count) {
               const span = document.createElement('span');
               span.innerHTML = '完成' + count;
               document.body.appendChild(span);
               document.body.appendChild(documentFrame);
           }
           /*  const  targetImg = document.createElement('img');
                  targetImg.src = 'img/img3024-4032.jpg?index=' + img.index;
                  documentFrame.appendChild(targetImg);
            */
           context = null;
           canvas = null;
           img = null;
       };
       // 加载图片出错的处理
       img.onerror = (error) => {
       };
       // 84KB
       // img.src = 'img/img512-min.jpg?index=' + index;
       // 3.35MB
       img.src = 'img/img3024-4032.jpg?index=' + index;
       // img.src = 'img/img512-max.jpg?index=' + index;
       // 240kb
       // img.src = 'img/img512-zmax.jpg?index=' + index;
       return canvas;
   };
   // 循环创建对象
   const loopCreateObject = (n) => {
       for (let i = 0; i < n; i++) {
           queue.push(createObject(n));
       }
   };
   const input = document.querySelector('#jsNumber');
   const button = document.querySelector('#jsCreate');
   button.addEventListener('click', (event) => {
       event.preventDefault();
       const number = input.value;
       if (!Number.isNaN(Number(number))) {
           queue = [];
           loopCreateObject(Number(number));
           console.log(`创建${number}MB canvas成功`);
       }
   });
</script>
</body>
</html>


1 篇评论

发表我的评论