前言
性能优化,一直作为前端的一个热点问题,作为一个优秀的前端开发人员,性能优化时必备技能。本文将从减少http请求次数、减少单次请求资源大小、渲染优化、资源加载优化等四个大方向,下分诸多小方向,全面总结常用前端优化方法。
(内容较多请看目录)
减少http请求次数
1.浏览器缓存策略
浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
-
Memory Cache:是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。浏览器秉承的是“节约原则”,我们发现,Base64格式的图片,几乎永远可以被塞进memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的JS、CSS文件,也有较大地被写入内存的几率——相比之下,较大的JS、CSS文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。
-
Service Worker Cache:是一种独立于主线程之外的Javascript线程。它脱离于浏览器窗体,因此无法直接访问DOM。这样独立的个性使得 Service Worker的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为Service Worker Cache。
-
HTTP Cache:它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
强缓存是利用http头中的Expires和Cache-Control两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
协商缓存依赖于服务端与浏览器之间的通信。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是304。
-
Push Cache:是指 HTTP2 在 server push 阶段存在的缓存。Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
2.CDN
CDN 的核心点有两个,一个是缓存,一个是回源。
“缓存”就是说我们把资源 copy一份到CDN服务器上这个过程,“回源”就是说CDN发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN往往被用来存放静态资源。所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源。用户可以从一个较优的服务器获取数据,从而达到快速访问,并减少源站负载压力的目的。
另外,CDN的域名必须和主业务服务器的域名不一样,要不,同一个域名下面的Cookie各处跑,浪费了性能流量的开销,CDN域名放在不同的域名下,可以完美地避免了不必要的 Cookie 的出现!
3.图片处理
- sprite雪碧图:CSS雪碧图是以前非常流行的技术,把网站上的一些图片整合到一张单独的图片中,可以减少网站的HTTP请求数量,但是当整合图片比较大时,一次加载比较慢。随着字体图片、SVG图片的流行,该技术渐渐退出了历史舞台。
- base64图片编码:将图片的内容以Base64格式内嵌到HTML中,可以减少HTTP请求数量。但是,由于Base64编码用8位字符表示信息中的6个位,所以编码后大小大约比原始值扩大了 33%
- 字体图标:字体图标通过自己规定字体的unicode编码,找到文件后根据unicode码去查找绘制外形,以文字的形式代替图片。
4.文件合并
将公共的js、css样式合并为一个大文件。
根据不同页面的需求单独合并所需js、css文件。
5.减少重定向
尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载,降低了用户体验。
如果一定要使用重定向,如http重定向到https,要使用301永久重定向,而不是302临时重定向。因为,如果使用302,则每一次访问http,都会被重定向到https的页面。而永久重定向,在第一次从http重定向到https之后,每次访问http,会直接返回https的页面。
减少单次请求资源大小
6.css压缩、图片压缩、gzip压缩、js混淆等
css压缩,就是进行简单的压缩,压缩空白等。
图片压缩,主要也是减小体积,在不影响观感的前提下,可以删除一些无关紧要的色彩。另外可以使用webp格式图片。
gzip压缩主要是针对html文件来说的,它可以将html中重复的部分进行一个打包,多次复用。
js混淆可以有简单的压缩(将空白字符删除)、丑化(将一些变量缩小)、或者对js进行混淆加密。
渲染优化
7.优化css选择符
CSS 选择符是从右到左进行匹配的,比如 #myul li {}实际开销相当高。因此需要对选择符进行优化,主要有如下几方面:
- 避免使用通配符*,只对需要用到的元素进行选择。
- 少用标签选择器。如果可以,用类选择器替代。错误:#dataList li{} 正确:.dataList{}
- 关注可以通过继承实现的属性,避免重复匹配重复定义。
- 不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿。错误:.dataList#title 正确:#title
- 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。
8.减少回流和重绘次数
- 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
- 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
重绘不一定导致回流,回流一定会导致重绘。
9.减少DOM操作
从上面可以知道,DOM改变容易引起回流和重绘,因此我们要减少DOM操作。
例子剖析,如下代码:
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>我是一个小测试</span>' //我们每一次循环都调用 DOM 接口重新获取了一次 container 元素,额外开销
}
进化一
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){
container.innerHTML += '<span>我是一个小测试</span>'
}
进化二
考虑JS 的运行速度,比 DOM 快得多这个特性。我们减少 DOM 操作的核心思路,就是让 JS 去给 DOM 分压。
//减少不必要的DOM更改
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进行操作
content += '<span>我是一个小测试</span>'
}
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content
进化三
在 DOM Fragment 中,DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
进化四
当涉及到过万调数据进行渲染,而且要求不卡住画面,如何解决?
如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次。
setTimeout(() => {
// 插入十万条数据
const total = 100000
// 一次插入 20 条,如果觉得性能不好就减少
const once = 20
// 渲染数据总共需要几次
const loopCount = total / once
let countOfRender = 0
let ul = document.querySelector('ul')
function add() {
// 优化性能,插入不会造成回流
const fragment = document.createDocumentFragment()
for (let i = 0; i < once; i++) {
const li = document.createElement('li')
li.innerText = Math.floor(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
countOfRender += 1
loop()
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add)
}
}
loop()
}, 0)
10.使用事件委托
事件委托是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此,可以由父节点的监听函数统一处理多个子元素的事件。利用事件委托,可以减少内存使用,提高性能及降低代码复杂度。
11.节流和防抖
当用户进行滚动,触发scroll事件,用户的每一次滚动都将触发我们的监听函数。函数执行是吃性能的,频繁地响应某个事件将造成大量不必要的页面计算。因此,我们需要针对那些有可能被频繁触发的事件作进一步地优化。节流与防抖就很有必要了!
- 函数节流: 频繁触发,但只在特定的时间内才执行一次代码
- 函数防抖: 频繁触发,但只在特定的时间内没有触发执行条件才执行一次代码
//防抖函数
//节流函数
function throttle(fn,time){
var last = 0;
return function(){
var context = this;
var now = Date.now();
if (now - last >= time){
fn.apply(this, arguments);
last = now;
}
};
}
//防抖函数
function debounce(fn, time){
return function(){
var context = this;
clearTimeout(timeId);
timeId = setTimeout(function(){
fn.apply(context, arguements);
}, time);
};
}
资源加载优化
12.资源预加载和懒加载
- 懒加载会延迟加载资源或符合某些条件时才加载某些资源。
- 预加载是提前加载用户所需的资源,加速页面的加载速度,保证良好的用户体验。
资源懒加载和资源预加载都是一种错峰操作,在浏览器忙碌的时候不做操作,浏览器空间时,再加载资源,优化了网络性能。
13.css、js文件引用位置优化
- CSS文件放在head中,先外链,后本页。
- JS文件放在body底部,先外链,后本页。
- body中间尽量不写style标签和script标签。
- 处理页面、处理页面布局的JS文件放在head中,如babel-polyfill.js文件、flexible.js文件。