前端性能优化,顾名思义,就是对于前端的性能的优化。
OK,一句非常憨憨的废话
为什么要做性能优化?性能优化到底有多重要?网站的性能优化对于用户的留存率、转化率有很大的影响。所以能否做好性能优化是衡量一个前端开发很重要的维度。
可以将性能优化分为两个大的分类:
1.加载时优化
2.运行时优化
加载时性能
顾名思义加载时优化 主要解决的就是让一个网站加载过程更快,比如压缩文件大小、使用CDN加速等方式可以优化加载性能。检查加载性能的指标一般看:白屏时间和首屏时间:
白屏时间:指的是从输入网址, 到页面开始显示内容的时间。
首屏时间:指从输入网址, 到首屏页面内容渲染完毕的时间。
运行时性能
运行时性能是指页面运行时的性能表现,而不是页面加载时的性能。可以通过chrome开发者工具中的 Performance 面板来分析页面的运行时性能。
接下来就从加载时性能和运行时性能两个方面来讨论网站优化具体应该怎么做。
加载时性能优化
我们知道浏览器如果输入的是一个网址,首先要交给DNS域名解析(改天我们详谈DNS解析过程) -> 找到对应的IP地址 -> 然后进行TCP连接 -> 浏览器发送HTTP请求 -> 服务器接收请求 -> 服务器处理请求并返回HTTP报文 -> 以及浏览器接收并解析渲染页面
。从这一过程中,其实就可以挖出优化点,缩短请求的时间,从而去加快网站的访问速度,提升性能。
这个过程中可以提升性能的优化的点:
- DNS解析优化,浏览器访问DNS的时间就可以缩短
- 使用HTTP2
- 减少HTTP请求数量
- 减少http请求大小
- 服务器端渲染
- 静态资源使用CDN
- 资源缓存,不重复加载相同的资源
从上面几个优化点出发,有以下几种实现性能优化的方式。
1.DNS预解析
DNS协议就是指将url的域名转化为相应的IP地址,供浏览器去访问的协议,那毫无疑问这个转化的过程是需要时间的,那么DNS预解析就是指预先将网络域名转化为IP地址,当你真正要访问的时候这部分时间将可以被大大缩短。
DNS预解析的实现:
用meta信息来告知浏览器, 当前页面要做DNS预解析:
<meta http-equiv="x-dns-prefetch-control" content="on" />
在页面header中使用link标签来强制对DNS预解析:
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
2.使用HTTP2
HTTP2带来了非常大的加载优化,所以在做优化上首先就想到了用HTTP2代替HTTP1。
HTTP2相对于HTTP1有这些优点:
解析速度快
服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。
多路复用
在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。
当然HTTP1.1有一个可选的Pipelining
技术,说的意思是当一个HTTP连接在等待接收响应时可以通过这个连接发送其他请求。听起来很棒,其实这里有一个坑,处理响应是按照顺序的,也就是后发的请求有可能被先发的阻塞住,也正因此很多浏览器默认是不开启Pipelining
的。
HTTP1 的Pipelining
技术会有阻塞的问题,HTTP/2的多路复用可以粗略的理解为非阻塞版的Pipelining。即可以同时通过一个HTTP连接发送多个请求,谁先响应就先处理谁,这样就充分的压榨了TCP这个全双工管道的性能。加载性能会是HTTP1的几倍,需要加载的资源越多越明显。当然多路复用是建立在加载的资源在同一域名下,不同域名神仙也复用不了。
首部压缩
HTTP2 提供了首部压缩功能。
HTTP 1.1请求的大小变得越来越大,有时甚至会大于TCP窗口的初始大小,因为它们需要等待带着ACK的响应回来以后才能继续被发送。HTTP/2对消息头采用HPACK(专为http/2头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络的流量。而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。
服务器推送
服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。
3.减少HTTP请求数量
HTTP请求建立和释放需要时间。
HTTP请求从建立到关闭一共经过以下步骤:
- 客户端连接到Web服务器
- 发送HTTP请求
- 服务器接受请求并返回HTTP响应
- 释放连接TCP链接
这些步骤都是需要花费时间的,在网络情况差的情况下,花费的时间更长。如果页面的资源非常碎片化,每个HTTP请求只带回来几K甚至不到1K的数据(比如各种小图标)那性能是非常浪费的。
4.压缩、合并文件
- 压缩文件 -> 减少HTTP请求大小,可以减少请求时间
- 文件合并 -> 减少HTTP请求数量。
我们可以对html、css、js以及图片资源进行压缩处理,现在可以很方便的使用 webpack 实现文件的压缩:
- js压缩:UglifyPlugin
- CSS压缩:MiniCssExtractPlugin
- HTML压缩:HtmlWebpackPlugin
- 图片压缩:image-webpack-loader
提取公共代码
合并文件虽然能减少HTTP请求数量, 但是并不是文件合并越多越好,还可以考虑按需加载方式。什么样的文件可以合并呢?可以提取项目中多次使用到的公共代码进行提取,打包成公共模块。
可以使用 webpack的 splitChunk
插件 cacheGroups
选项。
5.采用svg图片或者字体图标
因为字体图标或者SVG是矢量图,代码编写出来的,放大不会失真,而且渲染速度快。字体图标使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便,还有一个优点是生成的文件特别小。
6.按需加载代码,减少冗余代码
按需加载
在开发SPA项目时,项目中经常存在十几个甚至更多的路由页面, 如果将这些页面都打包进一个JS文件, 虽然减少了HTTP请求数量, 但是会导致文件比较大,同时加载了大量首页不需要的代码,有些得不偿失,这时候就可以使用按需加载, 将每个路由页面单独打包为一个文件,当然不仅仅是路由可以按需加载。
根据文件内容生成文件名,结合 import 动态引入组件实现按需加载:
通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash]
,它将根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash]
也会发生变化。
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
},
减少冗余代码
一方面避免不必要的转义:babel-loader
用 include
或 exclude
来帮我们避免不必要的转译,不转译node_moudules
中的js文件,其次在缓存当前转译的js文件,设置loader: 'babel-loader?cacheDirectory=true'
7.服务器端渲染
客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。
服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。
优点:首屏渲染快,SEO 好。缺点:配置麻烦,增加了服务器的计算压力。
8. 使用 Defer 加载JS
尽量将 CSS 放在文件头部,JavaScript 文件放在底部
所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。
那为什么 CSS 文件还要放在头部呢?
因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。
另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。
9. 静态资源使用 CDN
用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面, CDN就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
10. 图片优化
图片懒加载(监听页面滚动,等到可视区的时候再加载),雪碧图,webp等
运行时性能优化
1. 减少重绘与重排
有前端经验的开发者对这个概念一定不会陌生,浏览器下载完页面需要的所有资源后, 就开始渲染页面,主要经历这5个过程:
- 解析HTML生成DOM树
- 解析CSS生成CSSOM规则树
- 将DOM树与CSSOM规则树合并生成Render(渲染)树
- 遍历Render(渲染)树开始布局, 计算每一个节点的位置大小信息
- 将渲染树每个节点绘制到屏幕上
重排
当改变DOM元素位置或者大小时, 会导致浏览器重新生成Render树, 这个过程叫重排
重绘
当重新生成渲染树后, 将要将渲染树每个节点绘制到屏幕, 这个过程叫重绘。
了解了重排和重绘这两个概念,我们还要知道重排和重绘的开销都是非常昂贵的,如果不停的改变页面的布局,就会造成浏览器消耗大量的开销在进行页面的计算上,这样容易造成页面卡顿。那么回到我们的问题如何减少重绘与重排呢?
1.避免table布局
2.读写分离操作
多个DOM的读操作或者写操作应该放到一起,不要在两个读操作中加一个写操作
3.样式集中改变
不要频发的操作样式,虽然现在大部分浏览器有渲染队列优化,但是在一些老版本的浏览器仍然存在效率低下的问题
4.position属性设置为absolute或fixed
使用绝对定位会使的该元素单独成为渲染树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排
除了减少重绘重排以外,还有滚动事件性能优化
前端最容易碰到的性能问题的场景之一就是监听滚动事件并进行相应的操作。由于滚动事件发生非常频繁,所以频繁地执行监听回调就容易造成JavaScript执行与页面渲染之间互相阻塞的情况。
对应滚动这个场景,可以采用防抖
和节流
来处理。
当一个事件频繁触发,而我们希望间隔一定的时间再触发相应的函数时, 就可以使用节流(throttle)来处理。比如判断页面是否滚动到底部,然后展示相应的内容;就可以使用节流,在滚动时每300ms进行一次计算判断是否滚动到底部的逻辑,而不用无时无刻地计算。
当一个事件频繁触发,而我们希望在事件触发结束一段时间后(此段时间内不再有触发)才实际触发响应函数时会使用防抖(debounce)。例如用户一直点击按钮,但你不希望频繁发送请求,你就可以设置当点击后 200ms 内用户不再点击时才发送请求。
最后提到可以使用 Web Workers
前面提到了大量数据的渲染环节我们可以采用虚拟列表的方式实现,但是大量数据的计算环节依然会产生浏览器假死或者卡顿的情况.
通常情况下我们CPU密集型的任务都是交给后端计算的,但是有些时候我们需要处理一些离线场景或者解放后端压力,这个时候此方法就不奏效了.
还有一种方法是计算切片,使用 setTimeout 拆分密集型任务,但是有些计算无法利用此方法拆解,同时还可能产生副作用,这个方法需要视具体场景而动.
最后一种方法也是目前比较奏效的方法就是利用Web Worker 进行多线程编程.
Web Worker 是一个独立的线程(独立的执行环境),这就意味着它可以完全和 UI 线程(主线程)并行的执行 js 代码,从而不会阻塞 UI,它和主线程是通过 onmessage 和 postMessage 接口进行通信的。
Web Worker 使得网页中进行多线程编程成为可能。当主线程在处理界面事件时,worker 可以在后台运行,帮你处理大量的数据计算,当计算完成,将计算结果返回给主线程,由主线程更新 DOM 元素。
这么多关于性能优化的方面,有借助资料,这里只是做了一个汇总,也是为了加深自己对于性能优化的理解和运用。
希望各位看官们,不吝赐教!