前言
关于这个前端性能优化这个话题,我们不能为了优化而优化。按照一个流程:
-
为什么要做前端性能优化,它具体指代什么?
-
怎么去评估一个网站的前端性能?
-
性能优化分哪几个方向,具体有哪些点呢?
为什么/是什么
我们为什么要做性能优化?性能对于前端至关重要。性能在企业的成功中起着重要的作用。因为高性能网站比低性能网站更能吸引用户,并且性能是关于提高转化率重要指标。
有研究统计,某网站,主页加载速度每减低100毫秒,基于该页面的转换增加1.11%。平均收入增加380,000美元。支付页面加载速度每减低100毫秒,基于会话的转化率就增加1.55%,平均年收入增加530,000美元。
所以前端性能能影响公司的整体收入,同时,高性能的网站会给用户更好的用户体验。
我们提到的性能具体指代的是什么?实际上,性能是相对的,某个网站对于一个用户来说可能很快,但是对于另外一个用户来说可能很慢。也有可能页面很久加载出来,但是可以交互的时间却很长。所以需要有一些指标来衡量一个网站的性能。
在过去几年中,Chrome 团队成员(与W3C Web 性能工作组共同合作)一直致力于打造一组新的标准化 API 和指标,从而更准确地测量用户的网页性能体验。
几个重要指标:
First contentful paint 首次内容绘制(FCP): 测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。
Largest contentful paint 最大内容绘制(LCP): 测量页面从开始加载到最大文本块或图像元素在屏幕上完成渲染的时间。
First input delay 首次输入延迟(FID): 测量从用户第一次与您的网站交互(例如当他们单击链接、点按按钮或使用由 JavaScript 驱动的自定义控件)直到浏览器实际能够对交互做出响应所经过的时间。
Time to interactive 可交互时间(TTI): 测量页面从开始加载到视觉上完成渲染、初始脚本(如果有的话)完成加载,并能够快速、可靠地响应用户输入所需的时间。
Cumulative layout shift 累积布局偏移 (CLS):测量页面在开始加载和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数。
评估
怎么知道一个网站是否需要性能优化呢?可以使用 Lighthouse 进行性能检测(是否是headless chrome --- pupeteer进行检测),它可在几个关键领域测试网站 - 性能、可访问性、最佳实践以及您的网站作为渐进式 Web 应用程序的性能如何。
来看下[]淘宝网](淘宝网触屏版)的性能参数
某知名公司:
评估和webpack结合
性能结果和webpack结合加入构建过程。在构建步骤之后,webpack 输出资产及其大小的颜色编码列表。任何超出预算的都以黄色突出显示。
具体参考Incorporate performance budgets into your build process
性能优化
上面准备工作玩以后,我们来总结一下前端性能可以从哪些方面,哪些点进行优化。
试图从开发阶段的优化,然后再到上线以后网络层面,然后再到渲染层面进行性能优化的总结。
开发 --- > 网络 ---> 渲染
开发阶段
我们知道开发阶段主要是webpack的性能优化。
webpack性能优化
webpack的性能优化主要从打包时间和打包体积2个方面着手。
1. 升级webpack
首先要做的是升级webpack到最新版本,webpack5目前已经内置很多插件,并且进行了许多优化,升级其实是一个不错的选择。
2. 缩减搜索范围 / 减少文件处理
我们知道webpack会结合loader会去扫描各种文件,然后找到对应的loader进行转换。但是我们知道node_modules的文件是转译过后的,我们没必要再去扫描一边,以及一些引入到项目的第三方库。这些我们可以当作他们是以及成熟的文件不需要进行loader处理。
-
include/exclude
通常在各大Loader
里配置,src目录
通常作为源码目录,可做如下处理。当然include/exclude
可根据实际情况修改。
export default {
// ...
module: {
rules: [{
exclude: /node_modules/,
include: /src/,
test: /\.js$/,
use: "babel-loader"
}]
}
};
- 使用externals提取公共不会改变的库。在html里面引入对应的cdn即可。
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
enxternals这里会有2个功能:
-
防止将某些
import
的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。 -
暴露全局的jQuery变量名。有些代码里面可能会使用jQuery这样的变量名去取方法。挂在在全局则不会报错了。
-
使用DllPlugin将第三方包提前打包好。使用方法可以参考# webpack使用-详解DllPlugin。
DllPlugin大概意思就是,把不经常变换的库打包到一个文件中,并生成一个react.manifest.json文件。存着第三方库的name和打包后文件的对应位置关系,下次打包就不需要再经过读取,编译,转换等一系列耗时操作了。
3. 定向搜索
配置resolve提高文件的搜索速度
alias
映射模块路径,extensions
表明文件后缀,noParse
过滤无依赖文件。通常配置alias
和extensions
就足够。
export default {
// ...
resolve: {
alias: {
"#": AbsPath(""), // 根目录快捷方式
"@": AbsPath("src"), // src目录快捷方式
swiper: "swiper/js/swiper.min.js"
}, // 模块导入快捷方式
extensions: [".js", ".ts", ".jsx", ".tsx", ".json", ".vue"] // import路径时文件可省略后缀名
}
};
4. 缓存
配置cache缓存loader,好处是编译是只编译修改过的文件。大部分loader都提供了cache的选项,以babel-loader
和eslint-webpack-plugin
为例。
import EslintPlugin from "eslint-webpack-plugin";
export default {
// ...
module: {
rules: [{
// ...
test: /\.js$/,
use: [{
loader: "babel-loader",
options: { cacheDirectory: true }
}]
}]
},
plugins: [
new EslintPlugin({ cache: true })
]
};
5. 多线程
使用多线程的好处就是利用多核cpu并发处理文件的优势。我们知道js/node是单线程的,我们如何利用多核cpu来处理大量文件呢?
let HappyPack = require('happypack');
module.exports = {
...
module:{
rules:[
{
test:/\.js$/,
use:'HappyPack/loader?id=js'//这个id=js就代表这是打包js的
},
{
test:/\.css$/,
use:'HappyPack/loader?id=css'//这个id=css就代表这是打包css的
}
]
},
plugins:[
new HappyPack({这个id:js就代表这是打包js的
id:'css',//
use:['style-loader','css-loader']
}),
new HappyPack({这个id:js就代表这是打包js的
id:'js',//
use:[{//use是一个数组,这里写原先在rules的use里的loader配置
loader:'babel-loader',
options:{
presets:[
'@babel/presets-env',
'@babel/presets-react'
]
}
}]
})
]
}
6. 减少代码量 / 压缩
在准备代码上线的时候我们希望代码尽量少一些。我们可以剔除掉没有使用到的多余的代码。并且可以提取公共部分,这样相同代码不用重复打包在不同文件中增加代码体积。
export default {
// ...
mode: "production"
};
在webpack
里只需将打包环境设置成生产环境
就能让摇树优化
生效,同时业务代码使用ESM规范
编写,使用import
导入模块,使用export
导出模块。
压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频好处是更有效减少打包体积
- optimize-css-assets-webpack-plugin:压缩
CSS代码
- uglifyjs-webpack-plugin:压缩
ES5
版本的JS代码
- terser-webpack-plugin:压缩
ES6
版本的JS代码
webpack v4
使用splitChunks
替代CommonsChunksPlugin
实现代码分割。具体参考官网
export default {
// ...
optimization: {
runtimeChunk: { name: "manifest" }, // 抽离WebpackRuntime函数
splitChunks: {
cacheGroups: {
common: {
minChunks: 2,
name: "common",
priority: 5,
reuseExistingChunk: true, // 重用已存在代码块
test: AbsPath("src")
},
vendor: {
chunks: "initial", // 代码分割类型
name: "vendor", // 代码块名称
priority: 10, // 优先级
test: /node_modules/ // 校验文件正则表达式
}
}, // 缓存组
chunks: "all" // 代码分割类型:all全部模块,async异步模块,initial入口模块
} // 代码块分割
}
};
7. 按需加载
将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担
。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。
const Login = () => import( /* webpackChunkName: "login" */ "../../views/login");
const Logon = () => import( /* webpackChunkName: "logon" */ "../../views/logon");
// ----bable.config.js
{
// ...
"babel": {
// ...
"plugins": [
// ...
"@babel/plugin-syntax-dynamic-import"
]
}
}
网路层面
1. 减少 HTTP 请求
一个完整的http请求包含DNS查找,Tcp握手,客户端发送请求,服务器回应请求,浏览器等待响应。
名词解释:
- Queueing: 在请求队列中的时间。
- Stalled: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
- Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
- DNS Lookup: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
- Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
- SSL: 完成SSL握手所花费的时间。
- Request sent: 发出网络请求所花费的时间,通常为一毫秒的时间。
- Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间。
- Content Download: 接收响应数据所花费的时间 --- 13.05。
从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%
。
合并成大文件以后,这些开销所花费的时间不变,但是真正下载的比例变大了。比例越高代表这次http利用率更大,自然效率越高。
这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。
2. 使用 HTTP2
为什么要使用http2,它比http1.1有哪些有点呢?
解析速度快
服务器解析HTTP1.1的请求时,必须不断的读入字节,直到遇到分隔符CRLF为止。HTTP2的请求不会这么麻烦,因为HTTP2是基于帧的协议,每个帧都有表示帧长度的字段。
多路复用
对于HTTP1.1如果同时发起多个请求,就得建立多个TCP连接,因为一个TCP连接同时只能处理一个http请求。
在HTTP2上,多个请求可以共用一个TCP连接,这被称作多路复用。一个请求会有唯一的流ID来保证数据的正确。
首部压缩
HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送。
如果服务器收到了请求,它会照样创建一张表。
当客户端发送下一个请求的时候,如果首部相同,它可以直接发送首部key: 62 63 64。
服务器会查找先前建立的表格,并把这些数字还原成索引对应的完整首部。
索引 | 首部名称 | 值 |
---|---|---|
62 | Header1 | foo |
63 | Header2 | bar |
64 | Header3 | bat |
优先级
HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。
流量控制
带宽固定的情况下,一个请求占用的流量大了,那么另外一个请求的流量就会少。HTTP2可以对流占用的流量进行精确的控制,这样优先级高的可以快速处理完成。
-
优化图片
使用字体图标 iconfont 代替图片图标
字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。
参考资料:
选择正确的压缩级别
可以使用image-webpack-loader 来压缩图片:
{
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,
}
}
]
}
- 首选矢量格式:矢量图像与分辨率和比例无关,这使得它们非常适合多设备和高分辨率的世界。
- 缩小和压缩 SVG 资产:大多数绘图应用程序生成的 XML 标记通常包含可以删除的不必要的元数据;确保您的服务器配置为对 SVG 资产应用 GZIP 压缩。
- 比旧的光栅格式更喜欢 WebP:WebP 图像通常比旧图像小得多。
- 选择最佳光栅图像格式:确定您的功能要求并选择适合每个特定资产的格式。
- 试验光栅格式的最佳质量设置:不要害怕调低“质量”设置,结果通常非常好并且字节节省很大。
- 删除不必要的图像元数据:许多光栅图像包含有关资产的不必要的元数据:地理信息、相机信息等。使用适当的工具来剥离这些数据。
- 提供缩放图像: 调整图像大小并确保“显示”尺寸尽可能接近图像的“自然”尺寸。特别要注意大图像,因为它们在调整大小时占最大的开销!
- 自动化、自动化、自动化:投资自动化工具和基础设施,以确保您的所有图像资产始终得到优化。
用视频替换动画 GIF 以加快页面加载速度
GIF 和视频之间的成本节省可能非常显着。
动画 GIF 具有视频需要复制的三个关键特征:
-
它们会自动播放。
-
它们连续循环(通常,但可以防止循环)。
-
他们沉默。
幸运的是,您可以使用该
<video>
元素重新创建这些行为。
<video autoplay loop muted playsinline></video>
<video>
具有这些属性的元素会自动播放、无休止地循环播放、不播放音频并在线播放(即,非全屏播放),这是动画 GIF 所期望的所有标志性行为!🎉
兼容:
<video autoplay loop muted playsinline>
<source src="my-animation.webm" type="video/webm">
<source src="my-animation.mp4" type="video/mp4">
</video>
图片处理还提供以下优化参考连接:
[提供具有正确尺寸的图像](Serve images with correct dimensions)
tip: 图像策略
也许处理一张图像就能完爆所有构建策略
,因此是一种很廉价但极有效的性能优化策略
。
-
缓存
该缓存主要围绕浏览器缓存做相关处理,同时也是最低成本的性能优化策略。浏览器缓存能显著提升网页访问速度,减少带宽损耗。
静态资源使用 CDN
cdn的原理和优势具体参考# CDN是什么?使用CDN有什么优势?
渲染层面
渲染层面的性能优化,无疑是如何让代码解析更好执行更快,并且减少重绘重排。
浏览器渲染过程
- 解析HTML生成DOM树。
- 解析CSS生成CSSOM规则树。
- 将DOM树与CSSOM规则树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置大小信息。
- 将渲染树每个节点绘制到屏幕。
基于这些制造一些优化点:
- CSS策略:基于CSS规则
- DOM策略:基于DOM操作
- 阻塞策略:基于脚本加载
- 回流重绘策略:基于回流重绘
- 异步更新策略:基于异步更新
CSS策略
- 避免出现超过三层的
嵌套规则
- 避免为
ID选择器
添加多余选择器 - 避免使用
标签选择器
代替类选择器
- 避免使用
通配选择器
,只对目标节点声明规则 - 避免重复匹配重复定义,关注
可继承属性
DOM策略
-
缓存
DOM计算属性
-
避免过多
DOM操作
-
使用
DOMFragment
缓存批量化DOM操作
-
使用事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。
阻塞策略
-
脚本与
DOM/其它脚本
的依赖关系很强:对<script>
设置defer
-
脚本与
DOM/其它脚本
的依赖关系不强:对<script>
设置async
-
使用 Web Workers。Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。
回流重绘策略
- 缓存
DOM计算属性
- 使用类合并样式,避免逐条改变样式
- 使用
display
控制DOM显隐
,将DOM离线化
- 使用 flexbox 而不是较早的布局模型
- 使用 transform 和 opacity 属性更改来实现动画
- 使用 requestAnimationFrame 来实现视觉变化
异步更新策略
- 在
异步任务
中修改DOM
时把其包装成微任务
参考资料: