网站效果-阿里的福报

本页面正在开发中,所以时间回调一年…

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
<script>
(function () {
var style = document.createElement('style');
style.textContent = 'html,body{margin:0!important;height:100%!important}#app{position:fixed;inset:0;z-index:2147483647;background:#fff;overflow:auto}';
document.head.appendChild(style);
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://g.alicdn.com/CPO-platform/recruit-portal/1.0.6/css/index.css';
document.head.appendChild(link);
var app = document.createElement('div');
app.id = 'app';
document.body.appendChild(app);
window.g_config = window.g_config || {};
window.__userconfig = window.__userconfig || {};
window.__sysconfig = window.__sysconfig || {};
var srcs = [
'https://g.alicdn.com/code/lib/react/17.0.2/umd/react.production.min.js',
'https://g.alicdn.com/code/lib/react-dom/17.0.2/umd/react-dom.production.min.js',
'https://g.alicdn.com/code/lib/prop-types/15.8.1/prop-types.min.js',
'https://g.alicdn.com/CPO-platform/recruit-portal/1.0.6/js/index.js'
];
(function next(i) {
if (i >= srcs.length) return;
var s = document.createElement('script');
s.src = srcs[i];
s.onload = function () { next(i + 1); };
s.onerror = function () { next(i + 1); };
document.head.appendChild(s);
})(0);
})();
</script>

阿里招聘门户(g.alicdn.com/CPO-platform/recruit-portal/*/js/index.js)里有一段 three.js 的 WebGL 背景特效。它的渲染器在组件卸载时只删 DOM、不释放 WebGL 上下文,属于上下文泄漏;同一页面里反复挂载/卸载就会把浏览器每个页面的 WebGL 上下文额度(Chrome 约 16 个)耗尽,随后浏览器会丢弃最老的上下文,把同页面里别人的 WebGL 渲染一起搞挂。

出问题的代码

渲染器是每次挂载都重新 new 一个 canvas + context(NW.getRenderer):

1
2
3
4
5
6
7
NW.getRenderer = function (e, t, n = {}) {
var o = document.createElement("canvas");
var u = NW.useWebGL2
? o.getContext("webgl2", l)
: o.getContext("webgl", l) || o.getContext("experimental-webgl", l);
return new GB({ canvas: o, context: u, preserveDrawingBuffer: i, antialias: a }, t);
};

背景组件 init 调用它(注意 preserveDrawingBuffer: true,上下文更贵):

1
2
3
4
5
6
7
8
this.init = () => {
c = NW.getRenderer(p, false, { preserveDrawingBuffer: true });
c.setPixelRatio(u.dpr);
c.setSize(o, a);
c.setClearColor(14540253);
t.appendChild(c.domElement);
// ...
};

而卸载时没有 renderer.dispose() / forceContextLoss():

1
2
3
4
5
6
this.destroy = () => {
h.dispose(); // 只 dispose 了材质
m.dispose(); // 只 dispose 了几何
let e = t.childNodes;
for (let n = e.length - 1; n >= 0; n--) t.removeChild(e[n]); // 只移除 DOM
};

只要这个组件在同文档里被多次挂载(SPA 路由来回切、多实例、热更新等),上下文就会一直累积。Chrome 达到上限后控制台会出现:

1
WARNING: Too many active WebGL contexts. Oldest context will be lost.

并触发最老那个 canvas 的 webglcontextlost。

最小复现

下面这段就是同一套逻辑:一个 canvas 代表「你自己的 WebGL 渲染」,按钮每 120ms 模拟一次门户背景的挂载/卸载(创建 context,然后把 canvas 从 DOM 移除,但不释放 context)。到第 16 次左右,你的 canvas 会被浏览器丢弃。

修复方向

门户那边正确的做法是在 destroy() 里释放渲染器与上下文,例如:

1
2
3
4
5
6
7
8
9
this.destroy = () => {
h.dispose(); m.dispose();
if (c) {
c.dispose(); // 释放 three.js 资源
c.forceContextLoss(); // 主动丢失 WebGL 上下文
c.domElement.remove();
}
c = null;
};