You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

305 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 28 | Canvas、SVG与WebGL在性能上的优势与劣势
你好,我是月影。
性能优化,一直以来都是前端开发的难点。
我们知道前端性能是一块比较复杂的内容由许多因素决定比如网页内容和资源文件的大小、请求数、域名、服务器配置、CDN等等。如果你能把性能优化好就能极大地增强用户体验。
在可视化领域也一样,可视化因为要突出数据表达的内容,经常需要设计一些有视觉震撼力的图形效果,比如,复杂的粒子效果和大量元素的动态效果。想要实现这些效果,图形系统的渲染性能就必须非常好,能够在用户的浏览器上稳定流畅地渲染出想要的视觉效果。
那么针对可视化渲染,我们都要解决哪些性能问题呢?
## 可视化渲染的性能问题有哪些?
由于前端的可视化也是在Web上展现的因此像网页大小这些因素也会影响它的性能。而且无论是可视化还是普通Web前端针对这些因素进行性能优化的原理和手段都一样。
所以我今天想和你聊的是可视化方面特殊的性能问题。它们在我们熟悉的Web前端工作中并不常见通常只在可视化中绘制复杂图形的时候我们才需要重点考虑。这些问题大体上可以分为两类一类是**渲染效率问题,**另一类是**计算问题**。
**我们先来看它们的定义,渲染效率问题指的是图形系统在绘图部分所花费的时间,而计算问题则是指绘图之外的其他处理所花费的时间,包括图形数据的计算、正常的程序逻辑处理等等**。
我们知道在浏览器上渲染动画每一秒钟最高达到60帧左右。也就是说我们可以在1秒钟内完成60次图像的绘制那么完成一次图像绘制的时间就是1000/601秒=1000毫秒约等于16毫秒。
换句话说如果我们能在16毫秒内完成图像的计算与渲染过程那视觉呈现就可以达到完美的60fps即60帧每秒fps全称是frame per second是帧率单位。但是在复杂的图形渲染时我们的帧率很可能达不到60fps。
所以我们只能退而求其次最低可以选择24fps就相当于图形系统要在大约42毫秒内完成一帧图像的绘制。这是在我们的感知里达到比较流畅的动画效果的最低帧率了。要保证这个帧率我们就必须保证计算加上渲染的时间不能超过42毫秒。
因为计算问题与数据和算法有关,所以我们后面会专门讨论。这里,我们先关注渲染效率的问题,这个问题和图形系统息息相关。
我们知道Canvas2D、SVG和WebGL等图形系统各自的特点不同所以它们在绘制不同图形时的性能影响也不同会表现出不同的性能瓶颈。其实通过基础篇的学习我们也大体上知道了这些图形系统的区别和优劣。那今天我们就在此基础上深入讨论一下影响它们各自性能的关键因素理解了这些要素我们针对不同图形系统就能快速找到需要进行性能优化的点了。
## 影响Canvas渲染性能的2大要素
我们知道Canvas是指令式绘图系统它通过绘图指令来完成图形的绘制。那么我们很容易就会想到2个影响因素首先绘制图形的数量越多我们需要的绘图指令就越多花费的渲染时间也会越多。其次画布上绘制的图形越大绘图指令执行的时间也会增多那么花费的渲染时间也会越多。
这些其实都是我们现阶段得出的假设,而实践是检验真理的唯一标准,所以我们一起做个实验,来证明我们刚才的假设吧。
```
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
function randomColor() {
return `hsl(${Math.random() * 360}, 100%, 50%)`;
}
function drawCircle(context, radius) {
const x = Math.random() * WIDTH;
const y = Math.random() * HEIGHT;
const fillColor = randomColor();
context.fillStyle = fillColor;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
}
function draw(context, count = 500, radius = 10) {
for(let i = 0; i < count; i++) {
drawCircle(context, radius);
}
}
requestAnimationFrame(function update() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
draw(ctx);
requestAnimationFrame(update);
});
```
如上面代码所示我们在Canvas上每一帧绘制500个半径为10的小圆效果如下
![](https://static001.geekbang.org/resource/image/b2/e3/b278a4f98413b9029dfa914ab4b88be3.jpg "500个小球半径10")
注意为了方便查看帧率的变化我们在浏览器中开启了帧率检测。Chrome开发者工具自带这个功能我们在开发者工具的Rendering标签页中勾选FPS Meter就可以开启这个功能查看帧率了。
我们现在看到即使每帧渲染500个位置和颜色都随机的小圆形Canvas渲染的帧率依然能达到60fps。
接着我们增加小球的数量把它增加到1000个。
![](https://static001.geekbang.org/resource/image/8c/f9/8cbe753219d08a1b84753fe5f89518f9.jpg "1000个小球半径10")
这时你可以看到因为小球数量增加一倍所以帧率掉到了50fps左右现在下降得还不算太多。而如果我们把小球的数量设置成3000你就能看到明显的差别了。
那如果我们把小球的数量保持在500把半径增大到很大如200也会看到帧率有明显下降。
![](https://static001.geekbang.org/resource/image/ee/81/ee69d57ce2b1214f55650f8c3f8d9681.jpg "500个小球半径200")
但是单从上图的实验来看图形大小对帧率的影响也不是很大。因为我们把小球的半径增加了20倍帧率也就下降到33fps。当然这也是因为画圆比较简单如果我们绘制的图形更复杂一些那么大小的影响会相对显著一些。
通过这个实验我们能得出影响Canvas的渲染性能的主要因素有两点一是**绘制图形的数量**,二是**绘制图形的大小。**这正好验证了我们开头的结论。
总的来说Canvas2D绘制图形的性能还是比较高的。在普通的个人电脑上我们要绘制的图形不太大时只要不超过500个都可以达到60fps1000个左右其实也能达到50fps就算要绘制大约3000个图形也能够保持在可以接受的24fps以上。
因此在不做特殊优化的前提下如果我们使用Canvas2D来绘图那么3000个左右元素是一般的应用的极限除非这个应用运行在比个人电脑的GPU和显卡更好的机器上或者采用特殊的优化手段。那具体怎么优化我会在下节课详细来说。
## 影响SVG性能的2大要素
讲完了Canvas接下来我们看一下SVG。
我们用SVG实现同样的绘制随机圆形的例子代码如下
```
function randomColor() {
return `hsl(${Math.random() * 360}, 100%, 50%)`;
}
const root = document.querySelector('svg');
const COUNT = 500;
const WIDTH = 500;
const HEIGHT = 500;
function initCircles(count = COUNT) {
for(let i = 0; i < count; i++) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
root.appendChild(circle);
}
return [...root.querySelectorAll('circle')];
}
const circles = initCircles();
function drawCircle(circle, radius = 10) {
const x = Math.random() * WIDTH;
const y = Math.random() * HEIGHT;
const fillColor = randomColor();
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', radius);
circle.setAttribute('fill', fillColor);
}
function draw() {
for(let i = 0; i < COUNT; i++) {
drawCircle(circles[i]);
}
requestAnimationFrame(draw);
}
draw();
```
在我的电脑上一台普通的MacBook Pro内存8GB独立显卡绘制了500个半径为10的小球时SVG的帧率接近60fps会比Canvas稍慢但是差别不是太大。
![](https://static001.geekbang.org/resource/image/01/fa/01f6d0d37a0aeabd8449dyyecfc4e2fa.jpg "SVG绘制500个小球半径10")
当我们将小球数量增加到1000个时SVG的帧率就要略差一些大概45fps左右。
![](https://static001.geekbang.org/resource/image/4e/b6/4efb6930462da79f36e913546f5eb1b6.jpg "SVG绘制1000个小球半径10")
乍一看似乎SVG和Canvas2D的性能差别也不是很大。不过随着小球数量的增加两者的差别会越来越大。比如说当我们将小球的个数增加到3000个左右的时候Canvas2D渲染的帧率依然保持在30fps以上而SVG渲染帧率大约只有15fps差距会特别明显。
之所以在小球个数较多的时候二者差距很大因为SVG是浏览器DOM来渲染的元素个数越多消耗就越大。
如果我们保证小球个数在一个小数值然后增大每个小球的半径那么与Canvas一样SVG的渲染效率也会明显下降。
![](https://static001.geekbang.org/resource/image/19/ea/19d991c1ee547d1f98fe2f504eaba1ea.jpg "SVG绘制500个小球半径200")
如上图所示当渲染500个小球时我们把半径增加到200帧率下降到不到20fps。
最终我们能得到的结论与Canvas类似影响SVG的性能因素也是相同的两点一是**绘制图形的数量**,二是**绘制图形的大小**。但与Canvas不同的是图形数量增多的时候SVG的帧率下降会更明显因此一般来说在图形数量小于1000时我们可以考虑使用SVG当图形数量大于1000但不超过3000时我们考虑使用Canvas2D。
那么当图形数量超过3000时用Canvas2D也很难达到比较理想的帧率了这时候我们就要使用WebGL渲染。
## 影响WebGL性能的要素
用WebGL渲染上面的例子我们不需要一个一个小球去渲染利用GPU的并行处理能力我们可以一次完成渲染。
因为我们要渲染的小球形状相同所以它们的顶点数据是可以共享的。在这里我们采用一种WebGL支持的批量绘制技术叫做**InstancedDrawing实例化渲染**。在OGL库中我们只需要给几何体数据传递带有instanced属性的顶点数据就可以自动使用instanced drawing技术来批量绘制图形。具体的操作代码如下
```
function circleGeometry(gl, radius = 0.04, count = 30000, segments = 20) {
const tau = Math.PI * 2;
const position = new Float32Array(segments * 2 + 2);
const index = new Uint16Array(segments * 3);
const id = new Uint16Array(count);
for(let i = 0; i < segments; i++) {
const alpha = i / segments * tau;
position.set([radius * Math.cos(alpha), radius * Math.sin(alpha)], i * 2 + 2);
}
for(let i = 0; i < segments; i++) {
if(i === segments - 1) {
index.set([0, i + 1, 1], i * 3);
} else {
index.set([0, i + 1, i + 2], i * 3);
}
}
for(let i = 0; i < count; i++) {
id.set([i], i);
}
return new Geometry(gl, {
position: {
data: position,
size: 2,
},
index: {
data: index,
},
id: {
instanced: 1,
size: 1,
data: id,
},
});
}
```
我们实现一个circleGeometry函数用来生成指定数量的小球的定点数据。这里我们使用批量绘制的技术一下子绘制了30000个小球。与绘制单个小球一样我们计算小球的position数据和index数据然后我们设置一个id数据这个数据等于每个小球的下标。
我们通过instanced:1的方式告诉WebGL这是一个批量绘制的数据让每一个值作用于一个几何体。这样我们就能区分不同的几何体而WebGL在绘制的时候会根据id数据的个数来绘制相应多个几何体。
接着,我们实现顶点着色器,并且在顶点着色器代码中实现随机位置和随机颜色。
```
precision highp float;
attribute vec2 position;
attribute float id;
uniform float uTime;
highp float random(vec2 co) {
highp float a = 12.9898;
highp float b = 78.233;
highp float c = 43758.5453;
highp float dt= dot(co.xy ,vec2(a,b));
highp float sn= mod(dt,3.14);
return fract(sin(sn) * c);
}
vec3 hsb2rgb(vec3 c){
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
varying vec3 vColor;
void main() {
vec2 offset = vec2(
1.0 - 2.0 * random(vec2(id + uTime, 100000.0)),
1.0 - 2.0 * random(vec2(id + uTime, 200000.0))
);
vec3 color = vec3(
random(vec2(id + uTime, 300000.0)),
1.0,
1.0
);
vColor = hsb2rgb(color);
gl_Position = vec4(position + offset, 0, 1);
}
```
上面的代码中的random函数和hsb2rgb函数我们都学过了整体逻辑也并不复杂相信你应该能看明白。
最后我们将uTime作为uniform传进去结合id和uTime用随机数就可以渲染出与前面Canvas和SVG例子一样的效果。
这个WebGL渲染的例子的性能非常高我们将小球的个数设置为30000个依然可以轻松达到60fps的帧率。
![](https://static001.geekbang.org/resource/image/84/6f/84be3a259d9d7dc0572cf8044029536f.jpg "WebGL绘制30000个小球半径10")
WebGL渲染之所以能达到这么高的性能是因为WebGL利用GPU并行执行的特性无论我们批量绘制多少个小球都能够同时完成计算并渲染出来。
如果我们增大小球的半径那么帧率也会明显下降这一点和Canvas2D与SVG一样。当我们将小球半径增加到0.8相当于Canvas2D中的200那么可以流畅渲染的数量就无法达到这么多大约渲染3000个左右可以保持在30fps以上这个效率仍比Canvas2D有着5倍以上的提升。小球半径增加导致帧率下降是因为图形增大片元着色器要执行的次数就会增多就会增加GPU运算的开销。
好了那我们来总结一下WebGL性能的要素。WebGL情况比较复杂上面的例子其实不能涵盖所有的情况不过不要紧我这里先说一下结论你先记下来我们之后还会专门讨论WebGL的性能优化方法。
首先WebGL和Canvas2D与SVG不同它的性能并不直接与渲染元素的数量相关而是取决于WebGL的渲染次数。有的时候图形元素虽然很多但是WebGL可以批量渲染就像前面的例子中虽然有上万个小球但是通过WebGL的instanced drawing技术可以批量完成渲染那样它的性能就会很高。当然元素的数量多WebGL渲染效率也会逐渐降低这是因为元素越多本身渲染耗费的内存也越多占用内存太多渲染效率也会下降。
其次在渲染次数相同的情况下WebGL的效率取决于着色器中的计算复杂度和执行次数。图形顶点越多顶点着色器的执行次数越多图形越大片元着色器的执行次数越多虽然是并行执行但执行次数多依然会有更大的性能开销。最后如果每次执行着色器中的计算越复杂WebGL渲染的性能开销自然也会越大。
总的来说WebGL的性能主要有三点决定因素**一是渲染次数,二是着色器执行的次数,三是着色器运算的复杂度。**当然,数据的大小也会决定内存的消耗,因此也会对性能有所影响,只不过影响没有前面三点那么明显。
## 要点总结
要针对可视化的渲染效率进行性能优化,我们就要先搞清影响图形系统渲染性能的主要因素。
对于Canvas和SVG来说影响渲染性能的主要是绘制元素的数量和元素的大小。一般来说Canvas和SVG绘制的元素越多性能消耗越大绘制的图形越大性能消耗也越大。相比较而言Canvas的整体性能要优于SVG尤其是图形越多二者的性能差异越大。
WebGL要复杂一些它的渲染性能主要取决于三点。
第一点是渲染次数渲染次数越多性能损耗就越大。需注意要绘制的元素个数多不一定渲染次数就多因为WebGL支持批量渲染。
第二点是着色器执行的次数,这里包括顶点着色器和片元着色器,前者的执行次数和几何图形的顶点数有关,后者的执行次数和图形的大小有关。
第三点是着色器运算的复杂度复杂度和glsl代码的具体实现有关越复杂的处理逻辑性能的消耗就会越大。
最后数据的大小会影响内存消耗所以也会对WebGL的渲染性能有所影响不过没有前面三点的影响大。
## 小试牛刀
1. 刚才我们用SVG、Canvas和WebGL分别实现了随机小球由此比较了三种图形系统的性能。但是我们并没说HTML/CSS你能用HTML/CSS来实现这个例子吗用HTML/CSS来实现在性能方面与SVG、Canvas和WebGL有什么区别呢从中你能得出影响HTML/CSS渲染性能的要素吗
2. 在WebGL的例子中我们采用了批量绘制的技术。实际上我们也可以不采用这个技术给每个小球生成一个mesh对象然后让Ogl来渲染。你可以试着用Ogl不采用批量渲染来实现随机小球然后对比它们之间的渲染方案得出性能方面的差异吗?
* * *
## 源码
[课程中详细示例代码](https://github.com/akira-cn/graphics/tree/master/performance-basic)