性能优化
1. CDN的概念
CDN(Content Delivery Network, 内容分发网络) 是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序以及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。
- 典型的CDN系统由下面三个部分组成:
- 分发服务系统:最基本的工作单元就是Cache设置,cache(边缘cache) 负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数据、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。
- 负载均衡系统:主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全部负载均衡和本地负载均衡。全局负载均衡主要根据用户就进性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡
- 运营管理系统:运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。
CDN的作用
(1) 在性能方法,引入CDN的作用在于
- 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
- 部分资源请求分配给了CDN,减少了服务器的负载
(2) 在安全方面,CDN有助于防御DDos、MITM等网络攻击
- 针对DDoS: 通过监控分析异常流量,限制其请求频率
- 针对MITM:从源服务器到CDN节点到ISP,全链路HTTPS通信
除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰) 等方面的优势。
二、懒加载
懒加载与预加载的区别
- 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载
- 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存中获取资源
懒加载的代码实现
1 | <div class="container"> |
三、回流与重绘
回流与重绘的概念及触发条件
(1) 回流
当渲染树种部分或者全部元素的尺寸、构造或者属性发生变化时,浏览器会重新渲染部分或者全部
文档的过程就称为回流。下面这些操作会导致回流:
- 页面的首次渲染
- 浏览器的窗口大小发生变化
- 元素的内容发生辩护
- 元素的尺寸或者位置发生辩护
- 元素的字体大小发生辩护
- 激活css伪类
- 查询某些属性或者调用某些方法
- 添加或者删除可见的DOM元素
在触发回流的时候,由于浏览器渲染页面是基于流式布局,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:
全局范围:从根节点开始,对整个渲染树进行重新布局
局部范围:对渲染树的某部分或者一个渲染对象进行重新布局
(2) 重绘
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。下面这些操作会导致回流:
color、background相关属性:background-image等
outline相关属性:outline-color outline-width text-decoration
border-radius、visibility、 box-shadow
注意:当触发回流时,一定会触发重绘,但是重绘不一定会引发回流
- 如何避免回流与重绘?
操作DOM时,尽量在低层级的DOM节点进行操作
不要使用table布局,一个小的改动可能会使整个table进行重新布局
使用CSS的表达式
不要频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档这种
将元素先设置display:none 操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM的操作不会引发回流和重绘
将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿件插着写。这得益于浏览器的渲染队列机制。
浏览器针对页面的回流与重绘,进行了自身的优化–渲染队列
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
如何优化动画
对于如何优化动画,我们知道,一般情况下。动画需要频繁的操作DOM,就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了
documentFragment是什么?用它跟直接操作DOM的区别是什么?
当我们把一个DocumentFRagment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中,和直接操作DOM相比,将DocumentFragment节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。
四、节流与防抖
对节流与防抖的理解
- 函数防抖是指在事件被触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
- 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节点可以使用在scroll函数的事件监听上, 通过事件节点来降低事件调用的频率。
防抖节流函数的应用场景
防抖
- 输入框搜索场景:用户搜索后需要请求后端接口,只执行最后输入的值
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次
节流
- 拖拽场景:固定事件内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器resize
- 动画场景:避免短时间内多次触发动画引起性能问题
实现节流函数和防抖函数
函数防抖的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function debounce(fn,wait) {
let timer = null
return function(){
let context = this
let args = [...arguments]
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if(timer) {
clearTimeout(timer)
timer = null
}
// 设置定时器 使事件间隔指定时间后执行
timer = setTimeout(() => {
fn.apply(context,args)
}, wait)
}
}函数节流的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31function throttle(fn, wait) {
let timer = null
return function() {
let context = this
let args = [...arguments]
if(!timer) {
timer = setTimeout(() => {
fn.apply(context, args)
timer = null
}, wait)
}
}
}
// 时间戳版
function throttle(fn, delay) {
let preTime = Date.now()
return function() {
let context = this
let args = [...arguments]
let nowTime = Date.now()
// 如果两次时间间隔超过了指定时间,则执行函数
if(nowTime - preTime >= delay) {
preTime = Date.now()
fn.apply(context,args)
}
}
}防抖+节流
防抖有时候因为触发太过频繁, 导致一次响应都没有。所以希望固定的事件必定给用户一个响应,于是就有了防抖+节点的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 防抖+节流
function throttle(fn, delay) {
let last = 0
let timer = null
return function(...args) {
let context = this
let now = new Date()
if(now - last < delay) {
if(timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
} else {
// 这个时候表示时间到了 必须给响应
last = now
fn.apply(context, args)
}
}
}
五、图片优化
如何对项目中的图片进行优化
不用图片。很多时候会使用很多修饰类图片,其实这类修饰图片完全可以用CSS去代替
对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用CDN加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
小图使用base64格式
将多个图标文件整合到一张图片中(雪碧图)
选择正确的图片格式:
对于能够显示WebP格式的浏览器尽量使用WebP格式。因为WebP格式具有更好的图像压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
小图使用PNG其实对于大部分图标这类图片,完全可以使用SVG代替
照片使用JPEG
六、Webpack优化
如何提高webpack的打包速度
优化Loader
对于Loader来说,影响打包效率首当其冲必属Babel。因为Babel会将代码转为字符串生成AST,然后对AST继续进行转变最后再生成新的代码,代码越大,转换代码越多,效率就越低。
首先我们优化Loader的文件搜索范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}对于Babel来说,希望只作用在JS代码上的,然后node_modules中使用的代码都是编译过的,所以完全没有必要再去处理一遍。
当然这样做还不够,还可以将Babel编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
1
loader: 'babel-loader?cacheDirectory=true'
HappyPack
受限于Node是单线程运行的,所以Webpack在打包的过程中也是单线程的,特别是在执行Loader的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将Lader的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]DIIPlugin
DIIPligin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DIIPlugin的使用方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}然后需要执行这个配置文件生成依赖文件,接下来需要使用DllReferencePlugin将依赖文件引入项目中
1
2
3
4
5
6
7
8
9
10
11// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}代码压缩
在webpack3中,一般使用UglifyJS来压缩代码,但是这个是单线运行的,为了加快效率,可以使用webpack-parllel-uglify-plugin来并行运行UglifyJS,从而提高效率。
在webpack4中,不需要以上这些操作了,只需要将mode设置为production就可以默认开始以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩JS代码,还可以压缩HTML、CSS代码,并且在压缩JS代码的过程中,我们还可以通过配置实现比如删除console.log这类代码的功能。
其他
可以通过一些小的优化点来加快打包速度
resolve.extensions: 用来表面文件后缀列表,默认查找顺序是[‘.js’,’.json’], 如果你的导入文件没有添加后缀就会按照这顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高德后缀排在前面
resolve.alias: 可以通过别名的方式来映射一个路径,能让Webpack更快找到路径
module.noParse: 如果你确定一个文件下没有其他依赖,就可以使用该属性让Webpack不扫描该文件,这种方式对于大型的类库很有帮助
如何减少Webpack打包体积
- 按需加载
在开发SPA项目的时候,项目中都会存在很多路由界面。如果将这些页面全部打包进一个JS文件的话,虽然将
多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了页面能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于loadash这种大型类型同样可以使用这个功能。按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个Promise,当Promise成功以后执行回调。
- Scope Hoisting
Scope Hoisting 会分析出模板之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
如果在webpack4中你希望开启这个功能,只需要启用optimization.concatenateMudules 就可以了
1 | module.exports = { |
Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码,比如:
1 | // test.js |
对于以上情况, test 文件中的变量b如果没有在项目中使用到的话,就不会被打包到文件中。如果使用Webpack4的话,开启生产环境就会自动启动这个优化功能。
如何用webpack来优化前端性能?
用webpack优化前端性能是值优化webpack的输出结果,让打包的最终结果在浏览器运行快速高校
- 压缩代码:删除多余的代码、注释、简化代码的代码的写法等等方式。可以利用css-loader?minimize 来压缩css
- 利用CDN加速:在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对应output参数和各loader的publicPath参数来修改资源路径
- Tree Shaking:将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数 –optimize-minimize 来实现
- Code Splitting:将代码按路由维度或者组件分块chunk,这样做到按需加载,同时可以充分利用浏览器缓存
- 提取公共第三方库:SplitChunksPlugin插件来进行公共模块抽取,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码
如何提高webpack的构建速度
- 多入口情况下,使用CommonsChunkPlugin来提取公共代码
- 通过externals配置来提取常用库
- 利用DIIPlugIn和DIIReferencePlugin预编译资源模块 通过DIIPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DIIReferencePlugin将预编译的模块加载尽量。
- 使用Happypack实现多线程加速编译
- 使用webpack-uglify-parallel来提示uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用了多核并行压缩来提示压缩速度
- 使用Tree-shaking和Scope Hoisting来剔除多余代码