主题
WebGL 概览
WebGL 是 OpenGL ES 2.0 的 Web 绑定:它把一套「状态机 + 可编程着色器」的绘图接口暴露给 JavaScript,渲染结果直接写进 <canvas> 的后备存储里。和 Canvas 2D 相比,它没有 fillRect、没有路径、没有文字——你想画的一切最终都要变成顶点和颜色,由 GPU 并行处理。
这一页回答三个问题:WebGL 和 Canvas 2D 的本质区别是什么、GPU 管线里到底哪两段是可编程的、以及"跑起来"需要什么环境。后续 4 页逐层展开:上下文 → 着色器 → 缓冲区 → 矩阵。
它是什么,不是什么
| 是 | 不是 |
|---|---|
| OpenGL ES 2.0 子集的 JS 绑定,立即模式绘图 | 场景图 / 保留模式 API(那是 SVG、Three.js 的事) |
基于 <canvas> 的 GPU 渲染上下文 | 2D 绘图 API(没有路径、渐变、文本排版) |
| 可编程管线:顶点 + 片元两段自定义程序 | GPGPU 计算接口(WebGL1 只能靠"画到纹理再读回"绕) |
| 一套跨平台的图形状态机 | 保证行为完全一致的抽象层(不同驱动细节有差异) |
一句话记忆
Canvas 2D = 你告诉浏览器"画个圆";WebGL = 你告诉 GPU"把这些顶点按这个程序涂成像素"。
CPU 光栅化 vs GPU 管线
同一张画面,两种技术的"工作发生地"完全不同:
| 维度 | Canvas 2D(CPU 光栅化,可能部分走 GPU) | WebGL(GPU 可编程管线) |
|---|---|---|
| 谁在算像素 | 主要是 CPU 上的光栅化库(Skia 等) | GPU 上成千上万个着色器核心 |
| 绘制单位 | 路径 / 矩形 / 文本等高级图元 | 三角形、线、点 |
| 每帧开销 | 与图元数量和覆盖面积成正比 | 与 draw call 数量、像素数、状态切换成正比 |
| 抗锯齿 | 自动 | 由 antialias 或自己写(MSAA / 后处理) |
| 相机 / 3D | 需要手算投影,几乎不现实 | 矩阵 + 裁剪空间,天然支持 |
| 适合 | 图表、图像处理、2D 游戏、UI 装饰 | 3D 场景、粒子、GPU 图像处理、上万图元 |
不要用"能不能跑 60fps"选型
Canvas 2D 画 200 个矩形比 WebGL 快得多(几乎没有启动成本);WebGL 的优势要等到图元上万或需要逐像素计算时才体现。反过来,WebGL 的启动成本(着色器编译、缓冲区上传)在只画几个图形时是纯亏的。
可编程管线的两个阶段
WebGL 的绘制流程可以粗看成:
顶点数据(JS 数组)
↓ bufferData / vertexAttribPointer
[顶点着色器] ← 每个顶点执行一次:算坐标、传数据给后面
↓ gl_Position(裁剪空间坐标)
图元装配 → 裁剪 → 屏幕映射 → 光栅化
↓ 顶点数据按重心坐标插值成"每个片元一份"
[片元着色器] ← 每个像素(更准确说是每个片元)执行一次:决定最终颜色
↓ gl_FragColor
深度测试 / 混合 → 写入帧缓冲(画布)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
| 阶段 | 输入 | 输出 | 数量级 |
|---|---|---|---|
| 顶点着色器 | attribute 顶点属性、uniform | gl_Position、varying | 调用次数 = 顶点数 |
| 光栅化(固定功能) | 三角形 + 插值参数 | 片元 + 插值后的 varying | 与覆盖的像素数成正比 |
| 片元着色器 | 插值后的 varying、uniform、纹理 | gl_FragColor | 调用次数 ≈ 覆盖的像素数 |
数量级差异是性能分析的第一原则
一个 100 万三角形的模型,顶点着色器跑 100 万次;同屏 200 万像素时片元着色器跑 200 万次以上。"把计算放在顶点着色器里"通常是有效的优化(代价是插值会把它变得平滑)。
一次绘制的最小调用序列
把两段着色器真正跑起来,需要按固定顺序调用下面这些函数(这里省略了错误检查,实际代码请参照示例):
js
// ① 上下文:所有状态与资源都挂在它身上
const gl = canvas.getContext('webgl', { antialias: true })
gl.viewport(0, 0, canvas.width, canvas.height)
gl.clearColor(0.04, 0.06, 0.11, 1)
// ② 着色器:编译 → 编译 → 链接 → 启用
const vs = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vs, vertexSource)
gl.compileShader(vs)
const fs = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(fs, fragmentSource)
gl.compileShader(fs)
const program = gl.createProgram()
gl.attachShader(program, vs)
gl.attachShader(program, fs)
gl.linkProgram(program)
gl.deleteShader(vs)
gl.deleteShader(fs)
gl.useProgram(program)
// ③ 数据:缓冲区 + 属性指针(把数据接到 attribute 上)
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.8, -0.7, -0.5, 0.7, -0.5]), gl.STATIC_DRAW)
const loc = gl.getAttribLocation(program, 'aPosition')
gl.enableVertexAttribArray(loc) // ← 忘了这一行就什么都画不出来
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0)
// ④ 绘制:清屏 → 上传 uniform → 提交图元
gl.clear(gl.COLOR_BUFFER_BIT)
gl.uniform3f(gl.getUniformLocation(program, 'uColor'), 0.3, 0.7, 1.0)
gl.drawArrays(gl.TRIANGLES, 0, 3)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
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
这 4 步做完之后,每帧只需要重复第 ④ 步。②③ 属于"初始化",绝不能放进 requestAnimationFrame 回调:每帧新建 program 或缓冲区是新手最常见的性能杀手(第 ④ 步只有 3 个调用,第 ② 步有 12 个)。
| 阶段 | 执行时机 | 内容 |
|---|---|---|
| 初始化 | 组件挂载时,一次 | 上下文、着色器、缓冲区、纹理、全局状态开关 |
| 每帧 | requestAnimationFrame | clear、useProgram、更新 uniform、draw* |
| 销毁 | 组件卸载时 | delete* 释放 GPU 资源、停止循环、解绑事件 |
"WebGL 只画三角形"是什么意思
drawArrays / drawElements 只接受 7 种图元:
| 图元 | 常量 | 说明 |
|---|---|---|
| 点 | gl.POINTS | 每个顶点一个方形点,大小由 gl_PointSize 决定 |
| 线 | gl.LINES | 每 2 个顶点一条独立线段 |
| 折线 | gl.LINE_STRIP | 相邻顶点依次相连 |
| 闭合折线 | gl.LINE_LOOP | 同 LINE_STRIP,最后再连回起点 |
| 三角形 | gl.TRIANGLES | 每 3 个顶点一个独立三角形 |
| 三角带 | gl.TRIANGLE_STRIP | 相邻三角形共享一条边 |
| 三角扇 | gl.TRIANGLE_FAN | 所有三角形共享第一个顶点 |
所有面、曲面、文字最终都被拆成三角形,因为三角形永远共面、永远是凸的,硬件光栅化最简单也最快。多个三角形拼出来的模型,顶点顺序(正/反面)还决定了背面剔除。
本节地图
| 页面 | 解决什么问题 | 关键示例 |
|---|---|---|
| 概览(本页) | 它是什么、能做什么、跑不起来怎么办 | hello-webgl、gl-info |
| 上下文与最小渲染流程 | 属性怎么选、尺寸怎么算、丢失了怎么办 | context-attrs、context-lost |
| GLSL 着色器基础 | 着色器怎么写、怎么排查编译错误 | shaders-fragment、shaders-varying |
| 缓冲区、属性与绘制 | 顶点数据怎么组织、怎么画 | buffers-draw、buffers-indexed |
| 矩阵、坐标系统与相机 | 3D 的坐标怎么算、相机怎么动 | matrices-mvp、matrices-orbit |
| 纹理与采样 | 贴图、采样参数、mipmap | 见后续页面 |
| 光照模型 | 法线、Lambert / Phong | 见后续页面 |
| 深度、面剔除与 3D 场景 | 遮挡关系、场景组织 | 见后续页面 |
| 渲染状态、混合与调试 | 混合、剔除、调试工具 | 见后续页面 |
| 工程实践与性能 | 批处理、资源管理、上线检查 | 见后续页面 |
运行环境要求
| 条件 | 说明 |
|---|---|
| 浏览器支持 | 所有现代浏览器(含移动端)都支持 WebGL 1.0 |
| 硬件加速必须开启 | 浏览器"禁用硬件加速"时 getContext('webgl') 直接返回 null,或退化为极慢的软件渲染 |
| 检查入口 | get.webgl.org(WebGL1)、get.webgl.org/webgl2(WebGL2) |
| 隐私模式 / 虚拟机 | 部分环境屏蔽 GPU,可能拿不到上下文 |
| 上下文可能丢失 | 驱动更新、系统休眠、GPU 崩溃都会让上下文失效,需要监听事件并恢复 |
| 依赖 GPU 驱动 | RENDERER、各种 MAX_* 上限由驱动给出,必须运行时查询而不是写死 |
拿不到上下文不等于代码有 bug
canvas.getContext('webgl') 返回 null 只有两种可能:环境不支持,或者属性组合被拒绝。先给用户一块可读的提示,再谈降级到 Canvas 2D;静默白屏是最糟的处理方式(本站 createGL 就是这么做的)。
眼见为实:最小流程与实现信息
第一个示例把完整流程压缩到最短:获取上下文 → 编译着色器 → 上传 4 个顶点 → 每帧用 uniform 传参 → 画一个全屏渐变。
最小可运行流程全屏渐变矩形 + gl.getParameter 读回实现信息
examples/webgl/hello-webgl.jsjs
/**
* 【WebGL 1.0 · 概览】最小可运行流程
* 演示:createGL 获取上下文 → 编写/编译 GLSL → 上传顶点 → 用 uniform 传参 → drawArrays → 清理。
* 画面是一个全屏渐变矩形(TRIANGLE_STRIP 的 4 个顶点),下方列出 gl.getParameter 读到的实现信息。
*/
import { createGL, rafLoop, cleanupAll } from '../shared/runtime.js'
import { createProgram, introspect } from '../shared/gl.js'
import { createPanel } from '../shared/ui.js'
/** 顶点着色器:唯一必写的语句是给 gl_Position 赋值(裁剪空间坐标)。 */
const vertexSource = `
attribute vec2 aPosition;
varying vec2 vUv;
void main() {
vUv = aPosition * 0.5 + 0.5; // 裁剪空间 [-1,1] → UV [0,1]
gl_Position = vec4(aPosition, 0.0, 1.0); // 注意这里**没有**乘矩阵:坐标已经是裁剪空间
}
`
/** 片元着色器:必须声明浮点精度,必须给 gl_FragColor 赋值(或 discard)。 */
const fragmentSource = `
precision mediump float;
uniform vec3 uTop;
uniform vec3 uBottom;
uniform float uGlow;
varying vec2 vUv; // 由顶点着色器插值而来
void main() {
vec3 color = mix(uBottom, uTop, vUv.y); // mix / distance / smoothstep 都是内置函数
float d = distance(vUv, vec2(0.5));
color += uGlow * (1.0 - smoothstep(0.0, 0.75, d));
gl_FragColor = vec4(color, 1.0);
}
`
export default function (container) {
// 1) 拿上下文:createGL 内部完成 getContext('webgl')、viewport 同步与可选清屏
const view = createGL(container, {
version: 1,
width: 640,
height: 360,
antialias: true,
clearColor: [0.04, 0.06, 0.11, 1]
})
const { gl } = view
// 2) 着色器:编译 + 链接(失败时工具函数会抛出带行号的日志)
const program = createProgram(gl, vertexSource, fragmentSource)
const loc = introspect(gl, program)
// 3) 顶点数据:全屏矩形只需要 4 个顶点;TRIANGLE_STRIP 会自动组成 2 个三角形
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
// 4) uniform 参数(每帧上传,值可以随时间变化)
const top = [0.22, 0.47, 0.98]
const bottom = [0.02, 0.03, 0.09]
const glow = 0.32
function draw(time) {
gl.clear(gl.COLOR_BUFFER_BIT)
gl.useProgram(program)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.enableVertexAttribArray(loc.aPosition)
gl.vertexAttribPointer(loc.aPosition, 2, gl.FLOAT, false, 0, 0)
gl.uniform3fv(loc.uTop, top)
gl.uniform3fv(loc.uBottom, bottom)
gl.uniform1f(loc.uGlow, glow * (0.75 + Math.sin(time * 1.5) * 0.25))
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
const loop = rafLoop(draw)
// 5) 读回上下文信息:这些字符串由驱动决定,可以用来判断跑在什么设备/后端上
const panel = createPanel(container, { title: '运行环境(gl.getParameter)', position: 'below' })
panel.readout(`VERSION ${gl.getParameter(gl.VERSION)}`)
panel.readout(`VENDOR ${gl.getParameter(gl.VENDOR)}`)
panel.readout(`RENDERER ${gl.getParameter(gl.RENDERER)}`)
const size = panel.readout('')
const describe = () =>
`后备存储 ${view.pixelWidth}×${view.pixelHeight}(CSS ${view.width}×${view.height} · dpr ${view.dpr})`
size.set(describe())
const off = view.onResize(() => size.set(describe()))
panel.note('RENDERER 常被浏览器脱敏成 "WebKit WebGL";想看真实显卡型号要用 WEBGL_debug_renderer_info(见下个示例)。')
// 6) 清理:缓冲区 / program 必须显式删除,帧循环必须停止
return cleanupAll(loop, view, panel, off, () => {
gl.deleteBuffer(buffer)
gl.deleteProgram(program)
})
}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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
第二个示例只做一件事:把当前上下文的能力上限和扩展列表打印出来。写任何稍复杂的渲染前,都值得先看一眼这块信息。
实现信息与扩展体检VERSION / RENDERER / MAX_* / 支持的扩展
examples/webgl/gl-info.jsjs
/**
* 【WebGL 1.0 · 概览】读取并展示实现信息与扩展
* 演示:gl.getParameter 的常用查询项、WEBGL_debug_renderer_info、getSupportedExtensions,
* 用普通 DOM 表格 / 卡片呈现(画布本身只做一次清屏)。
*/
import { createGL, cleanupAll } from '../shared/runtime.js'
/** 值得在启动时「体检」一遍的实现限制:数值来自驱动,写死上限会踩坑。 */
const PARAM_KEYS = [
'VERSION',
'VENDOR',
'RENDERER',
'SHADING_LANGUAGE_VERSION',
'MAX_TEXTURE_SIZE',
'MAX_CUBE_MAP_TEXTURE_SIZE',
'MAX_RENDERBUFFER_SIZE',
'MAX_VERTEX_ATTRIBS',
'MAX_TEXTURE_IMAGE_UNITS',
'MAX_COMBINED_TEXTURE_IMAGE_UNITS',
'MAX_VARYING_VECTORS',
'MAX_VERTEX_UNIFORM_VECTORS',
'MAX_FRAGMENT_UNIFORM_VECTORS',
'ALIASED_LINE_WIDTH_RANGE',
'ALIASED_POINT_SIZE_RANGE'
]
/** 常用扩展:不检测就直接调用 gl.getExtension 的返回值,是常见的「什么都没发生」来源。 */
const COMMON_EXTENSIONS = [
['OES_vertex_array_object', 'WebGL1 的 VAO,省去每次重设属性指针'],
['OES_element_index_uint', '索引可以用 Uint32Array,突破 65535 顶点'],
['WEBGL_depth_texture', '把深度渲染到纹理(阴影贴图)'],
['OES_standard_derivatives', '片元着色器里的 dFdx/dFdy(法线可视化、抗锯齿)'],
['EXT_texture_filter_anisotropic', '各向异性过滤,斜视地面更清晰'],
['ANGLE_instanced_arrays', '实例化绘制(WebGL1 的扩展版)'],
['WEBGL_lose_context', '手动模拟上下文丢失,用于测试恢复逻辑'],
['EXT_sRGB', 'sRGB 纹理与帧缓冲']
]
export default function (container) {
const view = createGL(container, {
version: 1,
width: 640,
height: 120,
clearColor: [0.04, 0.06, 0.11, 1]
})
const { gl } = view
function format(value) {
if (value && (Array.isArray(value) || ArrayBuffer.isView(value))) return Array.from(value).join(' ~ ')
return String(value)
}
const rows = PARAM_KEYS.map((key) => [key, gl.getParameter(gl[key])])
// 隐私保护:RENDERER 常被脱敏,调试扩展能拿到真实型号(也用于判断是否走软件渲染)。
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
if (debugInfo) {
rows.push(['UNMASKED_VENDOR_WEBGL', gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)])
rows.push(['UNMASKED_RENDERER_WEBGL', gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)])
}
const box = document.createElement('div')
box.className = 'demo-dom'
const table = document.createElement('table')
table.style.width = '100%'
table.style.borderCollapse = 'collapse'
table.style.fontSize = '13px'
for (const [key, value] of rows) {
const tr = document.createElement('tr')
const keyCell = document.createElement('td')
keyCell.className = 'demo-mono'
keyCell.textContent = key
keyCell.style.padding = '3px 12px 3px 0'
keyCell.style.whiteSpace = 'nowrap'
keyCell.style.color = 'var(--vp-c-text-3)'
const valueCell = document.createElement('td')
valueCell.className = 'demo-mono'
valueCell.textContent = format(value)
valueCell.style.padding = '3px 0'
tr.append(keyCell, valueCell)
table.appendChild(tr)
}
box.appendChild(table)
const extensions = gl.getSupportedExtensions() || []
const heading = document.createElement('p')
heading.innerHTML = `当前上下文支持 <span class="demo-badge">${extensions.length}</span> 个扩展`
heading.style.margin = '14px 0 6px'
box.appendChild(heading)
const grid = document.createElement('div')
grid.className = 'demo-grid'
for (const [name, usage] of COMMON_EXTENSIONS) {
const card = document.createElement('div')
card.className = 'demo-card'
card.style.fontSize = '12.5px'
const title = document.createElement('p')
title.style.margin = '0 0 4px'
title.innerHTML = `${extensions.includes(name) ? '✅' : '➖'} <span class="demo-mono">${name}</span>`
const desc = document.createElement('p')
desc.className = 'demo-hint'
desc.style.textAlign = 'left'
desc.textContent = usage
card.append(title, desc)
grid.appendChild(card)
}
box.appendChild(grid)
const all = document.createElement('p')
all.className = 'demo-hint'
all.style.textAlign = 'left'
all.style.marginTop = '10px'
all.textContent = `全部扩展:${extensions.join('、') || '(无)'}`
box.appendChild(all)
container.appendChild(box)
// 计算能力的上限通常来自这些限制,而不是「显卡不够强」:
// MAX_TEXTURE_SIZE 决定单张贴图边长,MAX_VERTEX_ATTRIBS 决定一次能传几个属性。
return cleanupAll(view, () => box.remove())
}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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
API 速查
| 方法 | 参数 | 说明 |
|---|---|---|
canvas.getContext('webgl', attrs?) | 见下一节的属性表 | 返回 WebGLRenderingContext;失败返回 null(不抛错) |
canvas.getContext('experimental-webgl') | — | 老浏览器兼容写法,现代浏览器不需要 |
gl.getParameter(pname) | VERSION / VENDOR / RENDERER / MAX_* | 读回实现信息与能力上限 |
gl.getSupportedExtensions() | — | 返回字符串数组,全部可用扩展名 |
gl.getExtension(name) | 扩展名 | 返回扩展对象或 null,必须判空 |
gl.getContextAttributes() | — | 回读实际生效的上下文属性(可能与请求值不同) |
gl.compileShader(shader) / linkProgram(program) | — | 编译 / 链接,错误要看 InfoLog |
gl.drawArrays(mode, first, count) | 图元常量 | 按顺序取顶点绘制 |
gl.drawElements(mode, count, type, offset) | 通常 UNSIGNED_SHORT | 按索引缓冲绘制 |
gl.clear(mask) | COLOR_BUFFER_BIT 等 | 清屏;默认帧缓冲不会自动清 |
常见坑与排查
画布是黑的,控制台也没有报错
WebGL 的大部分错误不会抛异常,只写进 gl.getError()。按顺序排查:
canvas.getContext('webgl')是否为null(环境问题);- 着色器编译/链接是否成功(
COMPILE_STATUS/LINK_STATUS+ InfoLog); gl.getError()是否有INVALID_OPERATION(常见于没useProgram、没enableVertexAttribArray);gl_Position是否落在裁剪空间[-1, 1]内、w是否为正(被裁掉的图元不会报错);- 是否忘了
gl.clear,或清了但没画(preserveDrawingBuffer: false时旧内容不会保留)。
在 4K 屏 / 高 DPR 设备上画面糊或发虚
画布的后备存储尺寸(canvas.width/height)和显示尺寸(CSS 的 width/height)是两回事。只改 CSS 或只改 canvas.width 都会导致拉伸。本站的 createGL 已经按 devicePixelRatio 同步了两者并设置好 viewport。
上线前的三件小事
- 用
gl.getParameter(gl.MAX_TEXTURE_SIZE)校验用户上传的图片尺寸,而不是假定 4096 够用; - 监听
webglcontextlost,因为移动端切后台被回收显存是常态; - 页面卸载时把
program/buffer/texture全部delete*,否则反复进出页面会把显存吃光。