- 記錄自己解決的第一個H5頁的性能問題, 關於內存溢出
- 拼字游戲
問題表現
-
初始化后, 第一次拼字並不卡.
-
隨着拼的次數越來越多, 越來越卡
-
瀏覽器任務管理器中可以看出, 內存持續升高
-
確定內存問題, 即是卡頓第一問題
代碼層面問題
- 首先希望從代碼層面排查內存問題
- 思考代碼后, 發現以下兩點
第二舞台直接刪除Canvas的Dom
- 問題:
- 每次都會重新建立第二個分享舞台.
- 上一次的canvas直接刪除了Dom.
- 推斷:
- 推斷直接刪除DOM, 並不能釋放舞台上一系列的Bitmap圖像等
- 上幾次圖像繼續占用內存
- 排查與解決:
- 游戲舞台與分享舞台只保留一個
- 每次游戲, 通過指針清空bitmap
- 結果:
- 內存無明顯變化
loader進來的臨時圖片未刪除
- 問題:
- 每次完成一次拼字都會請求后台這次結果需要加載的資源
- 資源中包含大量的圖片等, 都是通過
new Image()
的src加載過來的
- 推斷:
- 每次瀏覽器加載大量圖片, 展示后, 並未釋放圖片空間
- 排查與解決:
- 找出不能重復利用的圖片, 使用新的加載器
- 一次性使用圖片后, 讓加載器為
null
- 結果:
- 內存少量減少
- 游戲已經卡頓
每次游戲會重新new一些對象
- 問題: 因每次進行游戲都會new一些對象出來
- 推斷: 懷疑是游戲過程中js內存溢
- 排查:
- 結論: js中的內存變化不能引起內存卡頓問題.
GPU層面問題
- 直接貼鏈接了: Chromium Graphics // Chrome GPU
- 這次通過任務管理器可以直接看出是GPU緩存的溢出
- 哭了, 為什么上次我測得時候不顯示... 僅僅是一個內存的表示, 給我好找啊..
- 最后還是在安卓中進行排查, 才知道是GPU / chromium grapics的問題
觀察GPU表現體征
- 我們在游戲中發現內存的溢出主要是GPU的變化
- 在游戲中去除所有的序列幀后, 只保留最基本的游戲, 任何手機都不卡.
- 那就一定是這些圖片搞得鬼.
- 已經把加載的圖片緩存清除掉了.
- 我們排除了canvas, 排除了圖片加載, 剩下的就是最基本的圖片展示了.
- 現在唯一剩下的就是瀏覽器自己做的事情了.
- 展示圖片, 然后清理DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
#stage {
width: 100%;
height: 100%;
}
</style>
<title>測試內存</title>
</head>
<body>
<button id="add">增加圖片</button>
<button id="clear">清除內存</button>
<button id="addTen">增加十倍圖片</button>
<div id="stage"></div>
<script>
var count = 0
var stage = document.getElementById('stage')
document.getElementById('add').onclick = function () {
for (var i = 0; i < 23; i++) {
stage.innerHTML += `<img src='./img${count}/${i}.jpg' />`
}
++count
}
document.getElementById('clear').onclick = function () {
stage.innerHTML = ''
count = 0
}
document.getElementById('addTen').onclick = function () {
for (var j = 0; j < 10; j++) {
for (var i = 0; i < 23; i++) {
stage.innerHTML += `<img src='./img${count}/${i}.jpg' />`
}
}
}
</script>
</body>
</html>
chrome中測試
- 在添加三組十倍圖片后, 清除DOM
- GPU和Image cache, 在瀏覽器中並不能總結出規律
webview表現特征
- 我們寫了一個最簡單的demo, 在安卓上的webview進行了測試
- 測試結果如下:
- 初始狀態下的內存:
- 添加圖片並瀏覽器后的內存:
- 清理頁面dom后的內存:
- 初始狀態下的內存:
總結
個人理解原理
瀏覽器中的GPU會自動緩存一段時間展示過的 "圖像". 我們稱為: "GPU Program Cache"
- 瀏覽器在讀取圖片之后會生成GPU可以使用的着色器代碼
- 在GPU使用的時候, 會自動緩存這些着色器代碼.
最后解決辦法
- 去掉大量序列幀, 序列幀盡量用小圖
- 動畫盡量用代碼實現
附帶一個測試着色器的代碼, 也是當時測試webgl的代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<canvas id="glcanvas" width="700" height="500"></canvas>
<script>
// 獲取WebGL
var canvas = document.getElementById('glcanvas')
gl = canvas.getContext("webgl")
// 創建頂點着色器
var VSHADER_SOURCE =
'void main() {\n' +
' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
// 創建片元着色器
// var FSHADER_SOURCE =
// 'void main() {\n' +
// ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
// '}\n';
// 繪制圓
var FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
void main() {
float d = distance(gl_PointCoord, vec2(0.5,0.5));
if(d < 0.5){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}else{ discard;}
}`;
// 着色器代碼需要放到一個程序中
var program = gl.createProgram()
// 創建頂點着色器
var vShader = gl.createShader(gl.VERTEX_SHADER)
// 創建片元着色器
var fShader = gl.createShader(gl.FRAGMENT_SHADER)
// shader容器與着色器綁定
gl.shaderSource(vShader, VSHADER_SOURCE)
gl.shaderSource(fShader, FSHADER_SOURCE)
// 將GLSE語言編寫成瀏覽器可用的代碼
gl.compileShader(vShader)
gl.compileShader(fShader)
// 將着色器添加到程序上
gl.attachShader(program, vShader)
gl.attachShader(program, fShader)
// 鏈接程序
// 在鏈接操作以后, 可以任意修改shader代碼.
// 對shader重新編譯不會影響整個程序, 除非重新鏈接程序
gl.linkProgram(program)
// 加載並使用鏈接好的程序
gl.useProgram(program)
// 繪制一個點
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
</script>
</body>
</html>