本篇文章参考以下博文
文章目录
前言
最近在学习过程中突然看到这个问题,也想总结以下自己关于性能优化的理解,这个问题比较大,我们需要逐步解耦来处理,回答需要有条理,有逻辑,才不至于涵盖面不全,有遗漏点。
首选我们要弄清楚一个问题,为什么要性能优化?或者说性能优化的目的是什么?那肯定是为了快速响应客户的操作。那么怎么样才能实现快速响应呢?可以从两方面来考虑,其一,提高页面加载时的速度,其二,提高页面运行时的速度。
除了上面两个方面,我个人觉得还可以从减少用户对网络延迟的感知方向入手。让用户减少不必要的 UI 感知。
一、减少延迟感知
这里我们以业界人机交互最顶尖的苹果举例,在 IOS 系统中:
点击“设置”面板中的“通用”,进入“通用”界面:
作为对比,再点击“设置”面板中的“ Siri 与搜索”,进入“ Siri 与搜索”界面:
你能感受到两者体验上的区别么?
事实上,点击“通用”后的交互是同步的,直接显示后续界面。而点击“ Siri 与搜索”后的交互是异步的,需要等待请求返回后再显示后续界面。但从用户感知来看,这两者的区别微乎其微。
这里的窍门在于:点击“ Siri 与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。
当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示 loading 的效果。
试想如果我们一点击“ Siri 与搜索”就显示 loading 效果,即使数据请求时间很短, loading 效果一闪而过。用户也是可以感知到的。
这方面的可以参考下 React 的 hook ——useDeferredValue
二、提高页面加载速度
2.1 减少 HTTP 请求
我们知道一次 HTTP 请求需要经历, DNS 查找, TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。以百度为例子,看一下一次请求的事件都花费在了哪:
名词解释:
- Queueing : 在请求队列中的时间。
- Stalled : 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
- Proxy negotiation : 与代理服务器连接进行协商所花费的时间。
- DNS Lookup : 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
- Initial Connection / Connecting : 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
- SSL : 完成SSL握手所花费的时间。
- Request sent : 发出网络请求所花费的时间,通常为一毫秒的时间。
- Waiting(TFFB) : TFFB 是发出页面请求到接收到应答数据第一个字节的时间。
- Content Download : 接收响应数据所花费的时间。
上面这写过程,是一次 HTTP 请求需要消耗的事件,那我们为了提高性能,尽量把一些相关的小文件,合并为一个大文件,用一次请求发送过来。
2.2 使用 HTTP2
为了加快响应速度,最直接的方法就是升级 HTTP ,目前最新的 HTTP2.0 ,相比之前,很多方面都进行了优化处理。
HTTP2 相比 HTTP1.1 有如下几个优点:
1.解析速度快
服务器解析 HTTP1.1 的请求时,必须不断地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦,因为 HTTP2 是基于帧的协议,每个帧都有表示帧长度的字段。
2.多路复用
HTTP1.1 如果要同时发起多个请求,就得建立多个 TCP 连接,因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。
在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。
多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。
3.首部压缩
HTTP2 提供了首部压缩功能。
一般同一个网页的很多请求,头部数据都是重复的。如果可以把相同的首部存储起来,仅发送它们之间不同的部分,就可以节省不少的流量,加快请求的时间。
HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。
当客户端发送请求时,它会根据首部值创建一张表:
如果服务器收到了请求,它会照样创建一张表。
当客户端发送下一个请求的时候,如果首部相同,它可以直接发送首部块:
服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。
4.优先级
HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。
5.流量控制
由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。
6.服务器推送
HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
例如当浏览器请求一个网站时,除了返回 HTML 页面外,服务器还可以根据 HTML 页面中的资源的 URL ,来提前推送资源。
2.3 使用服务端渲染
客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM ,再渲染。
服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML 。
- 优点:首屏渲染快, SEO 好。
- 缺点:配置麻烦,增加了服务器的计算压力。
一般网站都是首页做服务端渲染,这样可以被爬虫更方便的检索到。
服务端渲染的好处就是,页面是已经完成加载的状态,直接返回给浏览器就可以进行渲染了,不需要再对图片之类的其他资源再进行请求了。
2.4 静态资源使用 CDN
内容分发网络( CDN )是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。 CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
CDN 原理
当用户访问一个网站时,如果没有 CDN ,过程是这样的:
- 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
- 本地 DNS 依次向根服务器、顶级域名服务器、二级名称服务器、权限服务器发出请求,得到网站服务器的 IP 地址。
- 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并得到资源。
如果用户访问的网站部署了 CDN ,过程是这样的:
- 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
- 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统( GSLB )的 IP 地址。
- 本地 DNS 再向 GSLB 发出请求, GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统( SLB ),并将该 SLB 的 IP 地址作为结果返回给本地 。
- 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
- SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
- 浏览器再根据 SLB 发回的地址重定向到缓存服务器。
- 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。
2.5 将 CSS 放在文件头部,JavaScript 文件放在底部
所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。
那为什么 CSS 文件还要放在头部呢?
因为先加载 HTML 再加载 CSS ,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。
另外, JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。
2.6 使用字体图标 iconfont 代替图片图标
字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。
压缩字体文件
使用 fontmin-webpack 插件对字体文件进行压缩
2.7 善用缓存,不重复加载相同的资源
为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。
Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。
不过这样会产生一个问题,当文件更新了怎么办?怎么通知浏览器重新请求文件?
可以通过更新页面中引用的资源链接地址,让浏览器主动放弃缓存,加载新资源。
具体做法是把资源地址 URL 的修改与文件内容关联起来,也就是说,只有文件内容变化,才会导致相应 URL 的变更,从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢?我们会很自然的联想到利用“数据摘要算法”对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。
2.8 压缩文件
压缩文件可以减少文件下载时间,让用户体验性更好。
得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。
在 webpack 可以使用如下插件进行压缩:
JavaScript:UglifyPlugin
CSS :MiniCssExtractPlugin
HTML:HtmlWebpackPlugin
其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。
附上 webpack 和 node 配置 gzip 的使用方法。
下载插件
npm install compression-webpack-plugin --save-dev
npm install compression
webpack 配置
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [new CompressionPlugin()],
}
node 配置
const compression = require('compression')
// 在其他中间件前使用
app.use(compression())
2.9 图片优化
图片延迟加载
在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。
首先可以将图片这样设置,在页面不可见时图片不会加载:
<img data-src="https://avatars0.githubusercontent.com/u/22117876">
等页面可见时,使用 JS 加载图片:
const img = document.querySelector('img')
img.src = img.dataset.src
这样图片就加载出来了,完整的代码可以看一下参考资料。
响应式图片
响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。
通过 picture 实现
<picture>
<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
<source srcset="banner_w800.jpg" media="(max-width: 800px)">
<img src="banner_w800.jpg" alt="">
</picture>
通过 @media 实现
@media (min-width: 769px) {
.bg {
background-image: url(bg1080.jpg);
}
}
@media (max-width: 768px) {
.bg {
background-image: url(bg768.jpg);
}
}
调整图片大小
例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。
所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。
降低图片质量
例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。
压缩方法有两种,一是通过 webpack 插件 image-webpack-loader ,二是通过在线网站进行压缩。
以下附上 webpack 插件 image-webpack-loader 的用法。
webpack 配置
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
/*对图片进行压缩*/
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
尽可能利用 CSS3 效果代替图片
有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。
使用 webp 格式的图片
WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、 Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
2.10 Webpack 对于 JS 的相关优化
缓存(第二次访问速度更块)
- hash :文件打包每次都生成一个唯一哈希值,不管文件变不变。
- chunkhash :如果打包来自同一个入口,那么就属于同一个 chunk ,共享一个哈希值。
- contenthash :根据文件内容声称哈希值,文件内容不同,哈希值一定不同。
tree shaking
去除业务程序中没有使用的代码,让代码体积更小。(必须开启 es6 模块,生产环境会自动开启)
code split
单入口: js 文件代码会被加载成一个文件,体积过大,可以吧一个 bundle 拆分成多个,并行加载,提高速度。
多入口:有几个入口输出几个 bundle 。增加 optimization 后不会重复打包相同文件。
dll :对第三方库进行细化打包,生成多个文件。
懒加载 / 预加载
js 懒加载,用到才加载,不用不加载,异步中执行代码分隔。
js 预加载:其他资源加载完,再加载(兼容性有待提高)。
三.提高页面运行速度
3.1 减少回流和重绘
浏览器渲染过程
- 解析 HTML 生成 DOM 树。
- 解析 CSS 生成CSSOM 规则树。
- 将 DOM 树与 CSSOM 规则树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置大小信息。
- 将渲染树每个节点绘制到屏幕
回流
当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫回流。
重绘
当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致回流,例如改变字体颜色,只会导致重绘。记住,回流会导致重绘,重绘不会导致回流。
回流和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。
什么操作会导致回流?
- 添加或删除可见的 DOM 元素
- 元素位置改变
- 元素尺寸改变
- 内容改变
- 浏览器窗口尺寸改变
如何减少回流重绘?
- 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
- 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素( display:none )或文档碎片( DocumentFragement ),都能很好的实现这个方案。
3.2 使用事件委托
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。
3.3 节约内存
继承的运用:可以节约一大部分内存, CommonJS 使用寄生组合继承, ES6 使用类继承。
垃圾回收:对于不用的资源,使其指针指向 null 释放其内存。
3.4 Flex代替传统布局
这方面兼容性上需要注意。
四、总结
对于提高网页性能这个问题来说,包含的面非常广,几乎所有的新技术,目的都是为了提高网页性能,这个课题也是所有工程师共同追求的目标,所以当我们接触到这方面的问题时,需要涵盖各个方面,所以需要各位在平时工作学习中,注意积累,任何好的方法都可以记录下来,作为我们日后优化的手段。
当然所有的优化方法都有针对的情况,优化需要因地制宜,并不是把所有的优化手段都用上才是最好的,适合才好。