从五个方面聊聊前端开发中的性能优化

一、前言

性能可以用来评判一个互联网项目的质量,性能优化这个话题,可以从多个角度来分析,结合之前的工作经验,本篇文章主要是从延迟带宽DNS解析TCP/TLS协议项目静态资源这五个方面来做分析,如有发现有写的不对的地方,欢迎指出。

在谈优化之前,我们先来了解为什么现在的项目前端开发需要优化,想了想,大概是因为下面这三个原因:

  1. 页面展示内容多,页面中使用了更大更多的数据对象。
  2. 页面逻辑复杂。
  3. 在项目中引用了更多的三方资源。

前端的性能优化,可以从两个大的方面来谈

  1. 一方面是基于环境的优化,如网络环境、服务器资源等,这方面的问题是”硬件“的问题,在前端开发中是解决不了的;
  2. 另一方面是代码环境的优化:例如Javascript中的DOM 操作优化、CSS选择符优化、图片优化以及 HTML结构优化、按需加载等等。下面我们从这一方面来分析怎样做性能优化;

结合之前做过的项目,个人认为影响前端性能的关键因素会有以下几点

  1. 延迟,比如网络的延迟;
  2. 带宽,网络环境,流量控制;
  3. DNS解析,从域名解析成IP的时间。可以在终端里执行 ping 域名,查看域名的解析时间;
  4. TCP/TLS 安全传输协议,TLS是https协议中使用的,https可以防数据被劫持;
  5. 项目的静态资源,指代码压缩、合并等操作,前端可能更关注静态资源这块

接下来我们从这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的解析流程

  1. 查找浏览器缓存(这个步骤中前端可以做优化)。
  2. 查找系统缓存。
  3. 查找路由器缓存。
  4. 查找ISP DNS缓存。
  5. 迭代查询。

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 资源压缩

静态数据压缩传输,使用gzipbroti(谷歌推出的,比gzip好一些) 、http2.0,对数据进行压缩。

gzip设置
在请求头中设置:添加content-encoding:gzip,比如10k的文件,gzip压缩后只需要传4k。
在nginx中设置:server选项中添加gzip on

6.2 webpack处理静态资源

可以从两方面来考虑,一是减少webpack打包的时间,二是减少Webpack打包后的文件体积。

减少Webpack打包时间:

  1. 优化Loader的文件搜索范围,设置include、exclude。
  2. 把Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,loader: ‘babel-loader?cacheDirectory=ture’。
  3. HappyPack-plugin,开启多进程loader转换,将任务分解给多个子进程,最后将结果发给主进程。
  4. ParallelUglifyPlugin,开启多进程压缩js文件,每个子进程使用UglifyJS压缩代码,可以并行执行,能显著缩短压缩时间。
  5. DllPlugin,做分离打包,减少构建时间,提前编译好公共模块,放在指定位置。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。

减少Webpack打包后的文件体积:

  1. Scope Hoisting按需加载,它可以分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
  2. CommonsChunkPlugin,打包公共代码,将公共模块拆出来,存到缓存中供后续使用。webpack 4 中使用SplitChunkPlugin来代替CommonsChunkPlugin。
  3. url-loader,用来压缩图片,当图片资源过大时,url-loader是依赖于file-loader的。
  4. Tree Shaking,可以实现删除项目中未被引用的代码。
  5. webpack-bundle-analyzer,这是一个很有用插件,可以直观地查看每个模块占用资源的大小

6.3 缓存

缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。对于一个数据请求来说,可以分为发起网络请求后端处理浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

通常浏览器的缓存策略分为两种:强缓存协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

6.3.1 强缓存

强缓存可以通过设置两种 HTTP Header 实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200

  • Expires 是http1.0的规范,是一个绝对时间,到期后需要再次请求。受限于本地时间,如果修改了本地时间,可能会造成缓存失败。
  • Cache-Control 是http1.1的规范,它有多个属性,常用的属性max-age表示一个相对时间,以秒为单位,到期后需要再次请求。优先级高于Expires。可以在请求头或者响应头中设置。

6.3.2 协商缓存:

协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。

主要涉及到两组header字段:Last-ModifiedIf-Modified-SinceEtagIf-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项目常见的优化点

  1. 基于webpack打包优化:屏蔽sourceMap、treeshaking…
  2. 静态资源压缩传输
  3. 资源懒加载、预加载(组件、css、图片、。。)
  4. v-if 和 v-show选择调用
  5. 减少watch的数据,慎用deep watch
  6. SSR(服务端渲染)(根据业务需求)
  7. 骨架屏加载 (通过占位线框元素,渐进式加载数据)
  8. keep-alive 缓存

八、react项目常见的优化点

  1. shouldComponentUpdate & PureComponent 避免重复渲染
  2. 使用不可突变数据结构 immuable
  3. 组件尽可能的进行拆分、解耦
  4. bind函数优化
  5. 使用 React.lazy() 懒加载组件

关于前端的性能优化,暂时就先写这么多,有不对的地方欢迎指出。

猜你喜欢

转载自blog.csdn.net/Charissa2017/article/details/106737304