前端页面性能优化的基本思路

之前有一篇博文简单讲解了通过事件代理实现前端页面性能优化,通过事件代理进行优化只是前端页面优化的冰山一角,它只能代表某个具体的点,并不能覆盖到面,本篇博文将整理介绍前端性能优化的几个 ,分别从 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插件做流程化处理,非常方便。

优化选择器

选择器是从右向左匹配的,越深的选择器匹配所需时间越长,比如 .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预解析;
  • 非核心代码异步加载;
  • 利用缓存;

类似文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注