前端项目性能优化
性能优化大体可以分为两个方面
-
加载时优化
-
运行时优化
加载时优化
浏览器输入网址之后发生了什么
-
DNS域名解析
客户端收到域名地址后,首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。DNS采用的是UDP协议
-
建立TCP连接
三次握手:请求连接(SYN数据包),确认信息(SYN/ACK数据包),握手结束(ACK数据包)
-
发送HTTP请求
与服务器建立了连接后,就可以向服务器发起请求了。
-
服务器处理请求
服务器端收到请求后的由web服务器(准确说应该是http服务器)处理请求。
-
返回响应结果
在http里,有请求就会有响应,哪怕是错误信息。
在响应结果中都会有个一个http状态码,如200、301、404、500等。通过这个状态码可以知道服务器端的处理是否正常,并能了解具体的错误。
-
关闭TCP连接
为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。四次挥手。
-
浏览器解析HTML
浏览器通过解析HTML,生成DOM树,解析CSS,生成CSS规则树,然后通过DOM树和CSS规则树生成渲染树。
-
浏览器布局渲染
根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。
从中看到那些可以使用前端知识做优化的。
网络优化
http
超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。也就是说发起http请求前,都要连接上TCP,进行TCP3次握手,结束后要进行TCP的4次挥手。一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP :
这是一个 HTTP 请求,请求的文件大小为42.5KB。
名词解释:
- Queueing: 在请求队列中的时间。
- Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
- Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
- DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
- Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
- SSL: 完成SSL握手所花费的时间。
- Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
- Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。
- Content Download: 接收响应数据所花费的时间。
http1.0,http1.1以及http2.0的区别
-
http1.0,每次http请求都要建立一个TCP
-
http1.1,可以在一个TCP链接上可以传送多个http请求和响应,这样就不用多次建立和关闭TCP连接。但是多个http请求只能按顺序请求。
-
http2.0
-
多路复用:一个连接上可以有多个http请求,且可以随机的混在一起
-
header压缩: http1.x中的header需要携带大量信息.而且每次都要重复发送.http2.0使用encode来减少传输的header大小.而且客户端和服务端可以各自缓存(cache)一份header filed表,避免了header的重复传输,还可以减少传输的大小.
-
接下来对比一下http1.1和http2的
http1.1
http2
使用cdn
① 当用户点击应用程序上的内容时,应用程序将根据URL地址到本地 (域名解析系统)寻求IP地址解析。
② 本地DNS系统将域名解析给CDN专用DNS服务器。
③ CDN专用DNS服务器向用户返回CDN全局负载平衡设备的IP地址。
④ 用户向CDN负载平衡设备发起内容URL访问请求。
⑤ CDN负载平衡设备基于用户的IP地址和用户请求的内容URL在用户所属的区域中选择高速缓存服务器。
⑥ 负载平衡设备告诉用户缓存服务器的IP地址,并允许用户向选定的缓存服务器发起请求。
⑦ 用户向缓存服务器发起请求,缓存服务器响应用户的请求,并将用户所需的内容发送到用户终端。
⑧ 如果此缓存服务器没有用户需要的内容,则此缓存服务器将从网站的 请求内容。
⑨ 源服务器将内容返回到缓存服务器,缓存服务器发送给用户,并根据用户定义的缓存策略,确定是否在缓存服务器上缓存内容。
简单的说,就是DNS服务器给你分配一个最近最快的服务器IP地址,让你去获取资源
开启gzip
看一下开启前后的对比:
开启前
开启后
提高浏览器并发连接数
不同的浏览器对单个域名的最大并发TCP连接数有一定的限制(一般限制在4个左右,不同浏览器内核限制数量不一样),如果浏览器同时对某一域名发起多个请求,超过了限制就会出现等待。那么为了解决阻挡这一问题,可以对某些URL的域名分散处理。将不同的资源划分到不同的域名。
渲染优化
关键渲染路径优化
从收到 HTML、CSS 和 JavaScript 字节到对其进行必需的处理,从而将它们转变成渲染的像素这一过程中有一些中间步骤,优化性能其实就是了解这些步骤中发生了什么, 即关键渲染路径。优化关键渲染路径是指优先显示与当前用户操作有关的内容。
做关键渲染路径优化需要掌握的知识点
-
一个网页渲染的步骤
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局(Layout),以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
-
preload和prefetch
-
preload提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),需要执行时再执行
好处在于:
1、将加载和执行分离开,不阻塞渲染和document的onload事件
2、提前加载指定资源,不再出现依赖的font字体隔了一段时间才刷出的情况
-
prefetch告诉浏览器加载下一页面可能会用到的资源,注意,是下一页面,而不是当前页面。因此该方法的加载优先级非常低,也就是说该方式的作用是加速下一个页面的加载速度。
prefetch可以做DNS预解析:从域名查询IP的过程,这个过程一般都很快的,但也会引起延迟。一般浏览器会适当的对解析结果缓存,并对页面中出现的新域名进行预解析,但并不是所有的浏览器都会这么做,为了帮助其它浏览器对某些域名进行预解析,你可以在页面的html标签中添加dns-prefetch告诉浏览器对指定域名预解析
<link rel="dns-prefetch" href="//domain.com">
-
-
async和defer
- defer:延迟脚本。立即下载,但延迟执行(延迟到整个页面都解析完毕后再运行),按照脚本出现的先后顺序执行。
- async:异步脚本。下载完立即执行,但不保证按照脚本出现的先后顺序执行。
-
回流与重绘
当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树。完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘
关键渲染路径性能分析
描述关键渲染路径的词汇:
- 关键资源: 可能阻止网页首次渲染的资源。
- 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
- 关键字节: 实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。我们包含单个 HTML 页面的第一个示例包含一项关键资源(HTML 文档);关键路径长度也与 1 次网络往返相等(假设文件较小),而总关键字节数正好是 HTML 文档本身的传送大小。
图片等资源的影响
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="123.webp"></div>
</body>
</html>
DOMContentLoaded 事件的时间(5 毫秒),该时间同样与蓝色垂直线相符。HTML 下载结束与蓝色垂直线 (DOMContentLoaded) 之间的间隔是浏览器构建 DOM 树所花费的时间。
请注意,图片并没有阻止domContentLoaded 事件。这证明,并非所有资源都对首次绘制具有关键作用。事实上,关键渲染路径通常谈论的是 HTML 标记、CSS 和 JavaScript。
只需要DOM构建完成,就需要构建渲染树。
使用外部 CSS 文件
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critical Path: No Style</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="123.webp"></div>
</body>
</html>
html解析到link标签,所以style.css
在蓝色线前就开始请求
这里需要HTML 和 CSS 来构建渲染树,所以渲染树构建在style.css
请求返回并完成构建CSSOM后才开始。
使用外部的 Javascript 文件及 CSS 文件
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critical Path: No Style</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="123.webp"></div>
<script src="app.js"></script>
</body>
</html>
添加了 app.js,它既是网页上的外部 JavaScript 资源,又是一种解析器阻止(即关键)资源。更糟糕的是,为了执行 JavaScript 文件,我们还需要进行阻止并等待 CSSOM;
构建渲染树,需要等CSSOM构建完成后,同步的JS运行完毕后才开始构建。
优化方法
-
灵活使用async和defer延迟解析 JavaScript
-
CSS 标记为非关键资源
CSS 是构建渲染树的必备元素,首次构建网页时,JavaScript 常常受阻于 CSS。确保将任何非必需的 CSS 都标记为非关键资源(例如打印和其他媒体查询),并应确保尽可能减少关键 CSS 的数量,以及尽可能缩短传送时间。
-
将 CSS 置于文档 head 标签内
尽早在 HTML 文档内指定所有 CSS 资源,以便浏览器尽早发现 <link> 标记并尽早发出 CSS 请求。
webpack的分包策略、提高构建速度
分包策略
webpack 4默认使用optimization.splitChunks分包。默认的分割策略:
- 新的 chunk 是否被共享或者是来自 node_modules 的模块
- 新的 chunk 体积在压缩之前是否大于 30kb
- 按需加载 chunk 的并发请求数量小于等于 5 个
- 页面初始加载时的并发请求数量小于等于 3 个
默认的策略不能满足我们的需求,我们需要再进行个性化的优化。
寻找需要分割的包
vue-cli-service build --report
使用上面命令可以生成打包后的模块依赖及文件大小,确定优化的方向在哪。
抽离大文件
chainWebpack: (config) => {
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
design: {
name: 'chunk-design',
priority: 20,
test: /[\\/]node_modules[\\/]_?ant-design(.*)/
},
easemob: {
name: 'chunk-easemob',
priority: 20,
test: /[\\/]node_modules[\\/]_?easemob(.*)/
},
commons: {
name: 'chunk-commons',
minChunks: 2,
priority: 5,
chunks: 'initial',
reuseExistingChunk: true
}
}
})
},
抽离后的效果
CDN 方式
到https://www.bootcdn.cn/查找第三分cdn对接包的版本路径
-
在
index.html
引入相应 cdn 链接<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> --> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="referrer" content="never"> <meta name="renderer" content="webkit" /> <!-- <meta name="viewport" content="width=device-width,initial-scale=1.0"> --> <link rel="icon" href="<%= BASE_URL %>logo.png"> <title>熊猫进厂-专注普工招聘平台</title> </head> <body> <div id="app"> </div> <script src="./js/echarts.common.js"></script> <script src="./js/browser.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.runtime.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/ali-oss/6.15.2/aliyun-oss-sdk.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js"></script> <!-- built files will be auto injected --> </body> </html>
-
vue.config.js
配置externals
configureWebpack: config => { return { externals: { 'echarts': 'echarts', 'vue': 'Vue', 'ali-oss': 'ali-oss' } } },
打包后的情况
推荐使用CDN的方式
- 使用第三方CDN,有可能用户访问过其他网站使用了相同的库,就会有缓存,此时直接读缓存,
- 第三方CDN,和我们网站的域名不一样,浏览器可以创建新的连接过去文件
提高构建速度
使用speed-measure-webpack-plugin
分析打包时间的耗时主要是在哪里。配置
const path = require('path')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
function resolve (dir) {
return path.join(__dirname, dir)
}
// vue.config.js
const vueConfig = {
configureWebpack: config => {
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
return smp.wrap({
externals: {
'echarts': 'echarts',
'vue': 'Vue',
'ali-oss': 'ali-oss'
}
})
},
...
}
module.exports = vueConfig
可以看到本次打包用时20秒。
vuecli基本上没有什么优化的空间,如果自己配的webpack可以参考vuecli。
使用下面命令可以输出vuecli的配置
npx vue-cli-service inspect > output.js
-
缩小文件查找和处理范围
resolve: { alias: { '@': 'D:\\chitone\\panda-com-web\\src', vue$: 'vue/dist/vue.runtime.esm.js', '@$': 'D:\\chitone\\panda-com-web\\src' }, modules: [ 'node_modules', 'D:\\chitone\\panda-com-web\\node_modules', 'D:\\chitone\\panda-com-web\\node_modules\\@vue\\cli-service\\node_modules' ] }, module: { noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/, }
- 通过modules指定查找第三方模块的路径。
- 通过alias指定第三方模块直接查找到打包构建好的压缩js文件。
- 通过module指定noparse,对第三方模块不再进行分析依赖。
-
开启多线程打包
webpack4已经默认开启,4以下的版本可以使用
thread-loader
-
设置缓存:
cache-loader
module.exports = { module: { rules: [ { test: /\.js$/, use: [ 'cache-loader', 'babel-loader' ], include: path.resolve('src') } ] } }
Webpack 5已经内建了编译缓存的能力,并且 cache-loader 已弃用
DllPlugin 和 DllReferencePlugin
DllPlugin 可以把我们需要打包的第三方库打包成一个 js 文件和一个 json 文件,这个 json 文件中会映射每个打包的模块地址和 id,DllReferencePlugin 通过读取这个json文件来使用打包的这些模块。
优点:项目第三方库的依赖一般不会变的,打包后的文件名hash一般不会变,可以用户浏览器缓存
缺点:将多个第三方库的依赖打包在一起,文件的体积一般会比较大,影响首屏的显示。所以一般将多个体积小的依赖打包在一起,减少http请求,但是要注意包与包之间的冲突,打包一起后会报错。
运行时优化
- 虚拟长列表渲染
- 图片懒加载
- 使用事件委托