前端页面性能优化的基本思路
之前有一篇博文简单讲解了通过事件代理实现前端页面性能优化,通过事件代理进行优化只是前端页面优化的冰山一角,它只能代表某个具体的点,并不能覆盖到面,本篇博文将整理介绍前端性能优化的几个 面 ,分别从 HTML、CSS、JS 和 应用 本身四个角度来整理。
优化的含义
这是一个关键词前提,这里的优化具体指性能的优化,在表现上为页面打开更快,用户交互更流畅。
HTML 优化
涉及到 HTML 标签方面的优化我都整理为 HTML 优化。HTML标签的优化,可以围绕 加载、渲染、引用 三个方面来看,下面细化。
标签语义化
语义化的标签主要是为了实现两个目的,第一是对搜索引擎友好,第二是简化DOM结构;前面一种不属于性能优化,而是SEO,后面一种是性能优化,简化的DOM结构可以加快页面渲染。
避免无用嵌套,减少DOM节点
这是必须的,实现一种样式一定存在最少DOM结构,无需在最少DOM结构之上再去做多层嵌套,也无需更加无用的节点,节点越多渲染越慢,会影响性能。
表单结构使用 form 代替 div
如果DOM中存在表达结构,可以尽量使用 form 标签来代替 div ,例如:
<div>
<input id="username">
<input id="password">
</div>
上述表单外层的 div 标签会让 username 和 password 值的获取变得复杂,替换为 form 则可以简化取值。
<form id="login">
<input name="user-name">
<input name="password">
</form>
<script>
let form = document.getElementById('login'),
userName = form.username.value,
password = form.password.value;
</script>
减少 iframe 的使用
iframe虽然并行加载,具备沙箱保护机制,但iframe的缺点也是显而易见的:只要HTML中包含 iframe 标签,无论 iframe 的 src 属性是否为空,都会消耗加载时间;另外,iframe 没有语义,且更耗费资源,不是非必须的情况下应该尽量减少 iframe 的使用。
标签的 src 属性不要为空
具备 src 属性的标签,例如 img、script、iframe、video 等,尽量不要把 src 属性留空,如果 src 没有值,那么久不使用对应的标签,而非将 src 留空,src 属性会导致页面重载,影响载入速度。
正确使用 style 或 script
HTML 内联 style 标签会导致页面页面阻塞,同理 script 标签也是如此。如果有用到样式,可以通过异步的方式加载样式,或者将核心样式放到 head 内进行内联,非核心的样式等页面载入完成后再加载。script 标签可以放到body的尾部,已避免 script 阻塞页面。
CSS 优化
CSS优化指通过优化CSS已达到加快页面渲染的目的。
压缩样式文件
压缩样式文件是为了加快下载,下载速度一定的情况下,文件体积越小下载越快,这是压缩的目的。对于CSS文件的压缩,主要是去除文件中不需要的空格,以及样式块中最后一个属性的分号。一般压缩后的CSS文件体积可以减小到原来的几分之一,能明显加快下载的速度;
异步加载样式
异步的目的为了不阻塞页面的渲染,如果使用内联 style 标签来载入样式,那么有 style 标签的地方就会造成页面暂时性阻塞,一般可以将核心的CSS内联在DOM中,在页面渲染初期可以为用户提供一个较为友好的提示页面,非核心的样式文件通过 link 标签来异步载入。
剔除无用样式
有时候一个样式文件写多了以后会产生没有生效的样式,或者被覆盖的重复样式,这些样式都会导致样式文件体积过大,且重复渲染,这是可以通过工具将其中无用的样式剔除掉,已减小样式文件体积,同时避免样式覆盖导致的无效渲染。
uncss是一个高效的检查工具,可以集成到 gulp 上结合其他CSS插件做流程化处理,非常方便。
- 基于 gulp 的工程可以使用 gulp-uncss 插件;
- 基于 webpack 的工程可以使用 purifycss-webpack 插件配合 extract-text-webpack-plugin 来使用。
优化选择器
选择器是从右向左匹配的,越深的选择器匹配所需时间越长,比如 .login .username
就比 .login-username
更耗时间。因为前者需要先找到 .username
的元素,然后在剔除掉父元素不是 .login
的,而后者只需要找到 .login-username
的元素即可,试想如果选择器嵌套比较深,加上DOM结构复杂,那么渲染树的结构也将更深更复杂,渲染耗时也将更长。
不过以上只是逻辑分析,并不代表实际生产,实际中由于浏览器兼容性略有不同,加上浏览器对选择器优化也做了很多工作,所以选择器的速度差异不是特别明显,只要不犯不必要的错误即可,例如 .username 是 input 的类,那么使用 .login .username
就足够了,而非 .login input.username
这样的无用功。
最后也是比较找那个的一点,不用为了追求几毫秒的性能而放弃可读性和可维护性。
避免使用通配符
不适用通配符这点非常重要,应该始终保持用到什么就匹配什么的原则,通配符会导致浏览器遍历DOM中的所有标签,这也是一份不小的开支。
常见的通配符或者与通配符同等性质的,例如:
*
: {padding: 0} 中的 *- transition:
all
ease-in-out 中的 all
以上两种示例都应该避免使用。
避免或减少页面重排重绘
重复的样式被最新的样式覆盖,这看似来似乎不影响最后的表现,实则这会导致页面元素重新渲染和重排,所以应该避免无用的和重复的样式,这是第一。其次,一些无用的样式重置或者修改也会导致页面重绘,例如:
- 重设 font-size 或 font-family
- 重设 padding
- 重设 margin
- 改变元素的 class
- 获取元素的相关属性
- 激活CSS伪类
避免出现没必要的重绘的首要任务,就是在业务需要的地方,只改变需要的属性值,不变的属性值请勿重置。另外 flex 布局的重排速度更快,在不改变页面布局的情况下,使用 flex 布局比使用 inline-block 和 float 重排更高效。
一些用户操作也会导致页面重绘,例如翻页和鼠标滚动,以及交互效果等,这些是难以避免的,所以根据需要来进行优化即可。
避免使用 @import
使用 @import 引入样式文件会影响浏览器的并行下载。使用 @import 引用的CSS文件只有在引用它的那个CSS文件被下载、解析之后,浏览器才会知道还有另外一个CSS需要下载,这时才去下载,然后下载后开始解析、构建 render tree 等一系列操作,这就导致浏览器无法并行下载所需的样式文件。
JS 优化
JS主要是业务层面上使用更多,当一个页面初次渲染完成后,HTML和CSS的任务就暂时告一段落,之后的页面交互和渲染将主要交由 JS 脚本动态处理,所以优化 JS 先得尤为重要。
使用事件代理代替循环绑定
有关事件代理和循环绑定,我在另一篇博文 从事件冒泡到事件代理再到前端页面性能优化的一些思考 中简要分析,只要不是业务上非常必须,都建议使用事件代理绑定用户事件,而非循环绑定;即便需要循环绑定,也应该用具名函数的引用来关联事件回调,而非通过创建匿名函数来关联事件回调。
避免循环创建匿名函数
这个道理在实现列表时间绑定时有说过,每一个匿名函数都会占用系统资源,都会被分配函数指针,而循环绑定的匿名函数多数情况下在功能上是类似的,应该尽量避免。下面的代码是之前的一个例子:
这是常见做法,是不建议的:
<div>
<ul>
<li>list item</li>
<!-- 省略1000个 -->
<li>list item</li>
</ul>
</div>
<script>
document.querySelectorAll("li").forEach(function (element, index) {
// 这里使用匿名函数来关联 onclick 事件,不建议这样做
element.onclick = function (event) {
event = event || window.event;
console.log(index, event);
}
});
</script>
下面使用具名函数代替匿名函数的方式是建议的做法:
<div>
<ul>
<li>list item</li>
<!-- 省略1000个 -->
<li>list item</li>
</ul>
</div>
<script>
// 定义一个回调函数
function onLiClick (event, index) {
event = event || window.event;
console.log(index, event);
}
document.querySelectorAll("li").forEach(function (element, index) {
// 这里使用具名函数的引用,而不用创建匿名函数,节省了资源
element.onclick = onLiClick(event, index);
});
</script>
及时清理无用对象和变量
JavaScript的垃圾回收是自动的,一般是通过引用计数来判断某个对象是否需要被回收。也比较好理解,当一个变量没有在引用某个对象时,这个变量就不具备价值了,此时就可以被垃圾回收机回收掉。一般,当要回收某个对象时,将其指向 null
即可。
// 创建对象引用
let obj = new Object();
... // do something
// 指向 null ,之后会自动被回收
obj = null;
对于循环引用的情况,垃圾回收机是没有办法自动回收的,因为循环引用导致引用计数永远不为0,就永远不能为回收,此时需要手动断开循环引用,编程是也要避免循环引用的出现。
尽量使用内建函数
JS 是运行在解释器之上,虽然不同浏览器略有差异,但有一点是不变的,就是解释器使用 C/C++ 开发的,内建函数为了能提高速度也是使用 C/C++ 开发并内建好的,我们使用 JS 的一些内建函数的时候,实则是使用 C/C++ 编写的函数,这会比基于 JS 再去实现一套类似功能的函数更快,所以内建函数能实现的功能,请不要在用 JS 再实现一遍。
使用 switch 代替 if else
写 C 语言的同学应该知道,switch语句编译的那一刻每个分支就已经被分配好了,不存在递归查找的过程,也没有 if else 的判断,所以 switch case 语句可以更快的实现分支。这里的 JS 也是类似的,在多个分支之间没有复杂逻辑关系的情况下,使用 switch case 语句会比使用 if else 判断更高效。
优化循环条件
这一点在编程上经常用到,以 for 循环为例,一般为了方便可能会把循环的终止条件写在 for 循环里,但这并不是建议的,因为每次循环体执行完成后都需要检查一遍循环条件。距离说明一下:
let arr = [1, 2, 3, ...]; // 假设数组很长
for (let i = 0; i < arr.length; i++) {
console.log(i);
}
优化的做法可以是这样的:
let arr = [1, 2, 3, ...]; // 假设数组很长
let len = arr.length;
for (let i = 0; i < len; i++) {
console.log(i);
}
也可以优化为下面的情况:
let arr = [1, 2, 3, ...]; // 假设数组很长
let len = arr.length;
for (let i = arr.length; i >= l0; i--) {
console.log(i);
}
上面代码的循环条件是 i < arr.length ,length 会在每次循环体执行完以后获取并判断一次,提出到外部变量以后,length 只需要获取一次即可。
不过依然要说明一下,例子中由获取 length 带来的性能优化并不明显,所以不应该因此放弃代码的可读性和可维护性,但是对于循环条件相对复杂的情况,可以通过外置变量的形式来优化。
优化虚拟 DOM 的渲染时机
如果使用 JS 来动态创建 DOM ,会存在一个渲染时机的问题,例如动态创建一个 ul 标签,并给 ul 动态插入 100 个 li 标签,如果没插入一个 li 就渲染一次,势必会造成渲染的浪费,所以可以在完成插入所有 li 以后一次性进行渲染。
简化语句
这个很容易理解,能一条语句搞定的事情就不要使用一条即以上的语句,举个例子:
var num = total > 0 ? 1 : 0;
上面是一个三目运算符,是比较简洁的写法,如果使用 if else 改写,就会有点多此一举的意思。同理的还有类似 a++ 这样的语句。
简化对象的初始化
如果一个对象在初始化的时候可能明确推理出各个属性的值,那么就在初始化的时候一次性赋值,避免先初始化一部分,再通过属性赋值的方式再初始化一部分,这种方式不可取。
优化算法
JS中的算法复杂度:
标记名称 | 描述 | |
---|---|---|
O(1) | 常数 | 不管有多少值,执行的时间都是恒定的。一般表示简单值和存储在变量中的值 |
O(log n) | 对数 | 总的执行时间和值的数量相关,但是要完成算法并不一定要获取每个值。例如:二分查询 |
O(n) | 线性 | 总执行时间和值的数量直接相关。例如:遍历某个数组中的所有元素 |
O(n^2) | 平方 | 总执行时间和值的数量有关,每个值至少要获取n次。例如:插入排序 |
常数值和访问数组元素操作都是O(1)操作,对象属性查找操作是O(n)操作。
// O(1)操作
let values = [5, 10];
let sum = values[0] + values[1];
// O(2)操作
let values = window.location.href;
避免使用eval
eval 函数会增加内存消耗,非必须情况下不建议使用,如需外部引入 JS 模块,可以通过创建 script 标签的形式异步加载。
应用优化
除了从上述三个具体的方面做优化外,也可以从应用整体来做优化,简要概括可以包括如下几点:
- 减少 HTTP 请求;
- 使用CDN加速;
- DNS预解析;
- 非核心代码异步加载;
- 利用缓存;