一、前言
性能可以用来评判一个互联网项目的质量,性能优化这个话题,可以从多个角度来分析,结合之前的工作经验,本篇文章主要是从延迟
、带宽
、DNS解析
、TCP/TLS协议
、项目静态资源
这五个方面来做分析,如有发现有写的不对的地方,欢迎指出。
在谈优化之前,我们先来了解为什么现在的项目前端开发需要优化,想了想,大概是因为下面这三个原因:
- 页面展示内容多,页面中使用了更大更多的数据对象。
- 页面逻辑复杂。
- 在项目中引用了更多的三方资源。
前端的性能优化,可以从两个大的方面来谈:
- 一方面是基于环境的优化,如网络环境、服务器资源等,这方面的问题是”硬件“的问题,在前端开发中是解决不了的;
- 另一方面是代码环境的优化:例如Javascript中的DOM 操作优化、CSS选择符优化、图片优化以及 HTML结构优化、按需加载等等。下面我们从这一方面来分析怎样做性能优化;
结合之前做过的项目,个人认为影响前端性能的关键因素会有以下几点:
延迟
,比如网络的延迟;带宽
,网络环境,流量控制;DNS解析
,从域名解析成IP的时间。可以在终端里执行ping 域名
,查看域名的解析时间;TCP/TLS 安全传输协议
,TLS是https协议中使用的,https可以防数据被劫持;项目的静态资源
,指代码压缩、合并等操作,前端可能更关注静态资源这块;
接下来我们从这5个方面来分析怎样来做前端的性能优化。
二、针对延迟的优化
做CDN托管,CDN就是一个服务器,把资源同步到CDN,相当于在全国都有静态服务器,用户可以就近访问资源,与服务器的物理距离越近,延迟越低。
在实际操作中,一般来讲,运营商会提供一个用户名密码,我们只要把资源传上去,就会同步到全国各个服务器。
除了做CDN托管
外,还可以对资源进行缓存
,来减少延迟。(在第6.3章节中有讲缓存)。
三、针对带宽的优化
3.1 按需加载/延迟加载
延迟读取和执行脚本,延迟加载图片等,当需要的时候再对资源进行加载。
js文件延迟加载:在script标签上添加 async defer 等属性。
async
表示加载是异步的,不会阻塞页面渲染;defer
表示异步加载,在HTML解析完成后执行,defer的实际效果与将代码放在body底部类似
js文件按需加载:
原理:什么时候用,什么时候加载,每次加载后要将资源进行缓存,防止多次加载。也可以将脚本代码直接写在script标签内,不使用src,这样做的好处是减少请求。
场景:点击按钮弹出对话框,对话框里的js代码就可以当点击的时候再去加载。
对应的代码如下:
var obj ={};
/**
* 按需加载JS
* @param {string} url 脚本地址
* @param {function} callback 回调函数
*/
export function dynamicLoadJs (url, callback) {
if(obj[url]){
callback();
return;
}
obj[url]=true;
var head = document.getElementsByTagName('head')[0]
var script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
if (typeof (callback) === 'function') {
script.onload = script.onreadystatechange = function () {
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
callback()
script.onload = script.onreadystatechange = null;
}
}
}
head.appendChild(script);
}
图片按需加载 / 懒加载:判断当图片在可视区展示时,设置src属性,对应的代码如下:
//获取元素是否在可视区域
function isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth)
);
}
//如果当前图片在可视区域,执行loadImg
function checkImg() {
let imgs = document.querySelectorAll("img[lazy]");
Array.from(imgs).forEach(ele => {
if (isElementInViewport(ele)) {
loadImg(ele)
}
})
}
//设置图片的src
function loadImg(el) {
if (!el.src) {
let source = el.dataset.src;
el.src = source;
}
}
<img data-src="myimg.png" lazy>
3.2 资源预加载
preload
,是h5的新特性,用来做预加载资源
,浏览器在遇到如下link标签时,会立刻
开始下载
main.js(不阻塞渲染进程),并放在内存
中,但不会执行其中的JS语句。
只有当遇到script标签
加载的也是main.js
的时候,浏览器才会直接将预先加载的JS执行
掉。
<!--预加载js文件-->
<link rel="preload" as="script" href="./main.js">
<!--预加载css-->
<link rel="preload" as="style" href="./style.css">
<!--预加载字体-->
<link rel="preload" as="font" href="./font_zck.woff">
prefetch
,浏览器会在空闲
的时候,下载main.js, 并缓存
到disk。如果之后页面发生跳转
,跳转的目标页面引入
了main.js
,浏览器会直接从disk缓存
中读取
执行。
<link rel="prefetch" href="main.js">
如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次,所以不要在当前页面马上就要用的资源上用prefetch,要用preload。
除了使用preload 做资源的预加载,也可以使用iframe
做预加载
。在老浏览器的版本中,使用一个iframe,提前加载了跳转后的页面需要的资源。
3.3 调整图片大小
高分辨率的图片会浪费带宽、处理时间和缓存空间,动态调整图片大小或者替换成低分辨率的图片,在onload或者用户已经和页面开始交互的时候,再换成高分辨率的图片。
四、针对DNS解析的优化
DNS的解析流程:
- 查找浏览器缓存(这个步骤中前端可以做优化)。
- 查找系统缓存。
- 查找路由器缓存。
- 查找ISP DNS缓存。
- 迭代查询。
DNS预解析:
通过用meta标签
的信息来通知浏览器,这个页面要做DNS预解析
,让浏览器提前准备。
<meta http-equiv="x-dns-prefetch-control" content="on" />
也可以使用link标签
做DNS强制预解析
:
<link rel="dns-prefetch" href="http://ke.qq.com/" />
五、针对TCP/TLS的优化
优化思路:
- 减少页面的重定向,减少302跳转,页面重定向对性能消耗较大。比如用手机访问PC端,直接返回web页比传送一个重定向信息再让客户端请求要快;
- 减少设置代理时的 Rewrite;
- 减少TCP请求的个数,减小每个请求的大小不如减少请求的个数,
合并js/css文件
、使用图片雪碧图
、图片使用base64格式嵌入
等方式; - 使用http2.0协议;
http2.0协议的优点:
- 二进制传输,在http1.0/http1.1中,使用抓包工具可以看到,是通过
文本
的方式(十六进制
)传输数据的,而在http2.0协议中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制
格式编码。 - 对请求头压缩,使用HPACK(HTTP2头部压缩算法)压缩格式对请求头压缩,并在两端维护了
索引表
,用于记录出现过的header,后面在传输过程中就可以传输
已经记录过的header
的键名
,对端收到数据后就可以通过键名
找到对应的值
。 - 多路复用,在1.x中浏览器限会制同一个域名下的请求数量,一般我们会将不同的资源部署到不同的服务器上;而在http2.0中,有两个概念非常重要:帧(frame)和流(stream)。
帧是最小的数据单位
,每个帧会标识出该帧属于哪个流,流是多个帧组成的数据流
。所谓多路复用,即在一个TCP连接中存在多个流,即可以同时发送多个请求
,对端可以通过帧中的表示知道该帧属于哪个请求。在客户端,这些帧乱序发送,到对端后再根据每个帧首部的流标识符重新组装。通过该技术,可以避免HTTP旧版本的队头阻塞问题,极大提高传输性能。 - 服务器push,1.x中资源需要
客户端主动请求
,在http2.0中服务器可以主动推送
其他的资源。 - 更安全,http2.0对
TLS
的安全性做了近一步加强,默认支持https
,对https兼容更好。
http1.x的缺点:
- http1.0一次只允许在一个TCP连接上发起一个请求,http1.1使用的流水线技术也只能部分处理请求并发,仍然会存在
队列头阻塞
的问题,因此客户端在需要发起多次请求时,通常会采用建立多连接来减少延迟。 - 单向请求,只能由客户端发起请求。
- 请求头信息量大,请求报文与响应报文首部信息冗余量大。
- 数据没有压缩,导致数据的传输量大。
六、针对静态资源的优化
6.1 资源压缩
静态数据压缩传输,使用gzip
、broti
(谷歌推出的,比gzip好一些) 、http2.0
,对数据进行压缩。
gzip设置:
在请求头中设置:添加content-encoding:gzip
,比如10k的文件,gzip压缩后只需要传4k。
在nginx中设置:server选项中添加gzip on
。
6.2 webpack处理静态资源
可以从两方面来考虑,一是减少webpack打包的时间,二是减少Webpack打包后的文件体积。
减少Webpack打包时间:
- 优化Loader的文件搜索范围,设置include、exclude。
- 把Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,loader: ‘babel-loader?cacheDirectory=ture’。
- HappyPack-plugin,开启
多进程loader转换
,将任务分解给多个子进程,最后将结果发给主进程。 - ParallelUglifyPlugin,开启
多进程压缩js文件
,每个子进程使用UglifyJS压缩代码,可以并行执行,能显著缩短压缩时间。 - DllPlugin,做分离打包,减少构建时间,
提前编译好公共模块
,放在指定位置。这种方式可以极大的减少打包类库的次数
,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件
的优化方案。
减少Webpack打包后的文件体积:
- Scope Hoisting,
按需加载
,它可以分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。 - CommonsChunkPlugin,打包公共代码,将公共模块拆出来,存到缓存中供后续使用。webpack 4 中使用
SplitChunkPlugin
来代替CommonsChunkPlugin。 - url-loader,用来
压缩图片
,当图片资源过大
时,url-loader
是依赖于file-loader
的。 - Tree Shaking,可以实现
删除
项目中未被引用
的代码。 - webpack-bundle-analyzer,这是一个很有用插件,可以
直观地
查看每个模块
占用资源的大小
。
6.3 缓存
缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。对于一个数据请求来说,可以分为发起网络请求
、后端处理
、浏览器响应
三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
通常浏览器的缓存策略分为两种:强缓存
和协商缓存
,并且缓存策略都是通过设置 HTTP Header
来实现的。
6.3.1 强缓存
强缓存可以通过设置两种 HTTP Header 实现:Expires
和 Cache-Control
。强缓存表示在缓存期间不需要请求
,state code 为 200
。
- Expires 是http1.0的规范,是一个
绝对时间
,到期后需要再次请求。受限于本地时间,如果修改了本地时间,可能会造成缓存失败。 - Cache-Control 是http1.1的规范,它有多个属性,常用的属性
max-age
表示一个相对时间
,以秒为单位,到期后需要再次请求。优先级高于Expires
。可以在请求头或者响应头中设置。
6.3.2 协商缓存:
协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。
主要涉及到两组header字段:Last-Modified
和If-Modified-Since
、Etag
和If-None-Match
。
Last-Modify 和 If-Modify-Since
浏览器第一次
请求一个资源的时候,服务器返回的header
中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间
,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次
请求该资源时,request的请求头
中会包含If-Modify-Since
,该值为缓存之前返回的Last-Modify
。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
如果命中缓存,则返回304
,并且不会返回资源内容
,并且不会返回Last-Modify
。
Etag 和 If-None-Match
Etag/If-None-Match返回的是一个哈希值
。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化
。服务器根据浏览器上送的If-None-Match
值来判断是否命中缓存。ETag 优先级高于
Last-Modified。
与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
为什么要有Etag
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
6.4 Web Worker
Web Worker 的目的就是为了给 Javascript 创建多线程
的环境,允许主线程创建多线程,将一些任务交给别的线程执行,两者互不干扰
,避免
了主线程
即UI线程被阻塞
。
worker中的上下文和主线程js的上下文对象是不同的,window不是它的顶层对象,所以window相关的一些方法如alert等时不能使用的,还有dom也是不能访问的
。不过基本的方法。例如console.log、setTimeout等可以访问。
优点:避免主线程被阻塞。
缺点:不能操作DOM,不能改变页面的内容。
具体的使用方法可以查看:https://blog.csdn.net/Charissa2017/article/details/104682062
6.5 Service Worker
Service Worker 可以理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中我们可以做很多事情,比如拦截客户端的请求
、向客户端发送消息
、向服务器发起请求
等等,其中最重要的作用之一就是对资源进行缓存
。
优点:所有的资源都可以缓存。
缺点:缓存的资源必须是同域,需要是https的方式访问。
6.6 Manifest
Manifest,指HTML5的应用缓存,当第一次请求后,根据manifest文件
进行本地缓存
,并且在下一次请求后进行展示(若有缓存的话,无需再次进行请求而是直接调用缓存),缓存后,就算是在没有网络的环境下,也可以访问缓存的内容。
具体的使用方法可以查看:https://blog.csdn.net/Charissa2017/article/details/104614884
七、vue项目常见的优化点
- 基于webpack打包优化:屏蔽sourceMap、treeshaking…
- 静态资源压缩传输
- 资源懒加载、预加载(组件、css、图片、。。)
- v-if 和 v-show选择调用
- 减少watch的数据,慎用deep watch
- SSR(服务端渲染)(根据业务需求)
- 骨架屏加载 (通过占位线框元素,渐进式加载数据)
- keep-alive 缓存
八、react项目常见的优化点
- shouldComponentUpdate & PureComponent 避免重复渲染
- 使用不可突变数据结构 immuable
- 组件尽可能的进行拆分、解耦
- bind函数优化
- 使用 React.lazy() 懒加载组件
关于前端的性能优化,暂时就先写这么多,有不对的地方欢迎指出。