前端性能优化总结

优化的原则和方向

前端性能优化的原则其实就是更好的用户体验,具体实现的目标大体有两个:

  1. 合理使用内存或缓存,减少请求;
  2. 减少CPU或者GPU的计算,达到更快的展现。

前端在性能优化的方向大体有两个:

  1. 减少页面体积,提升网络加载
  2. 优化页面渲染

详情

减少页面体积,提升网络加载

1、静态资源的压缩合并(JS 代码压缩合并、CSS 代码压缩合并、雪碧图)

  1. 压缩是为了减小文件体积,减轻网络负载,达到更快的下载;
  2. 合并和雪碧图都是为了减少文件的请求次数,但不是合并的就一个比没有合并时加载快,要看合并之后的体积,若文件合并后太大了也不太利于性能优化,所以在实际的项目中要做好权衡。

2、静态资源缓存(资源名称加 MD5 戳)

可以通过链接名称控制缓存:通过前端构建工具为打包的文件添加md5后缀,这样当打包上线时请求的链接发生改变,这样可以防止由于缓导致静态资源更新失效;

3、 使用 CDN 让资源加载更快

优化页面渲染

1、CSS 放前面,JS 放后面

  1. 浏览器在渲染解析过程中,若遇到<link href="..."><script src="...">这种外链加载 CSS 和 JS 的标签,浏览器会异步下载并解析执行。CSS放在头部是为了让浏览器尽早解析执行Css文件,渲染出页面的样式,若放在底部会出现渲染卡顿的情况,影响性能和体验。
  2. 而当渲染过程中遇到script标签时就会执行JS代码,从阻塞页面渲染,因为浏览器渲染和 JS 执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染 DOM 冲突。所以要将JS放在底部,等到页面渲染完成之后再去解析执行js,保证用户体验性。因为浏览器渲染和 JS 执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染 DOM 冲突。
  3. 另外,JS 执行如果涉及 DOM 操作,得等待 DOM 解析完成才行,JS 放在底部执行时,HTML 肯定都解析成了 DOM 结构。JS 如果放在 HTML 顶部,JS 执行的时候 HTML 还没来得及转换为 DOM 结构,可能会报错。

2、懒加载(图片懒加载、下拉加载更多)

先将src赋值成一个通用的预览图,下拉时候再动态赋值成正式的图片。如下,preview.png是预览图片,比较小,加载很快,而且很多图片都共用这个preview.png,加载一次即可。待页面下拉,图片显示出来时,再去替换src为data-src的值。(data-开头的属性浏览器渲染的时候会忽略掉,提高渲染性能)

1
<img src="preview.png" data-src="realImg.png"/>

3、减少DOM 查询,对 DOM 查询做缓存

1
2
3
4
5
6
7
// 只查询一个 DOM ,缓存在 pList 中了
var pList = document.getElementsByTagName('p')
for (var i = 0; i < pList.length; i++) {
}
// 每次循环,都会查询 DOM ,耗费性能
for (var i = 0; i < document.getElementsByTagName('p').length; i++) {
}

4、减少DOM 操作,多个操作尽量合并在一起执行(DocumentFragment)

DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入 DOM。因为Fragment文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流。

1
2
3
4
5
6
7
8
9
10
var listNode = document.getElementById('list')
// 要插入 10 个 li 标签
var frag = document.createDocumentFragment();
var i, li;
for(i = 0; i < 10; i++) {
li = document.createElement("li");
li.innerHTML = "List item " + i;
frag.appendChild(li); //先放在 frag 中,最后一次性插入到 DOM 结构中。
}
listNode.appendChild(frag);

5、事件节流

在开发过程中会遇到页面一些频繁触发的事件,比如mouseover、scroll、resize等事件。一秒可以执行很多次,这样会造成严重的页面性能问题,导致页面c出现卡顿甚至浏览器崩溃。因此我们需要对事件进行节流,简单的说就是控制其执行的次数。这里就涉及到了常用到的js的节流和防抖功能实现。
1、 防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function debounce(fn, delay) {
// 定时器,用来 setTimeout
var timer
// 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
return function () {
// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments
// 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
timer && clearTimeout(timer)
// 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
// 再过 delay 毫秒就执行 fn
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}

2、节流(throttle):规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

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
function throttle(fn, threshhold) {
// 记录上次执行的时间
var last
// 定时器
var timer
// 默认间隔为 250ms
threshhold || (threshhold = 250)
// 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
return function () {
// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments

var now = +new Date()
// 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃
// 执行 fn,并重新计时
if (last && now < last + threshhold) {
timer && clearTimeout(timer)
// 保证在当前时间区间结束后,再执行一次 fn
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshhold)
// 在时间区间的最开始和到达指定间隔的时候执行一次 fn
} else {
last = now
fn.apply(context, args)
}
}
}

6、尽早执行操作(DOMContentLoaded)

1
2
3
4
5
6
7
8
9
window.addEventListener('load', function () {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完即可执行,此时图片、视频还可能没有加载完
})
$(document).ready({function () {
// 同DOMContentLoaded
})

7、使用 预渲染 或者 SSR后端渲染,数据直接输出到 HTML 中,减少浏览器使用 JS 模板渲染页面 HTML 的时间 (如Vue SSR),同时也有利于网站的SEO。