前端常用性能优化方向
关注公众号: 微信搜索 前端工具人
; 收货更多的干货
文章来源:自己掘金文章 https://juejin.cn/post/7080723026873942046/
文章整理于 20
年写的 性能优化文章,现添加补充以及详细说明;
用于面试、也可警惕自己日常开发都是个不错的选择;
有不对之处欢迎留言;
一、 vue 方向
v-if
和v-show
v-if
会导致重绘重排, 从DOM
树中删除、成本很大; 适应于初始渲染后续不在变化的DOM
;v-show
控制的是DOM
样式, 避免了重绘重排, 适用频繁切换显示、隐藏 的DOM
;
v-for
中一定要用key
v-for
渲染列表时,遵循就地复用策略;他会根据key
值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;- 不要使用
index
索引作为key
,index
会改变; 尽量使用数据唯一值;
v-for
和v-if
不要一起使用
v-for
的优先级高于v-if
; 即先执行完v-for
才会执行v-if
逻辑, 会增加 DOM渲染难度;- 用
template
包一层代替;
computed
计算属性代替watch
computed
计算属性的值有缓存,只有它依赖的属性值发生改变,才会触发获取逻辑;
- 路由懒加载 以及 KeepAlive
- 避免进入首页就加载全部的前端资源造成白屏时间过长;
- 静态页面(引导页、协议、介绍等页面)可设置
KeepAlive
包裹,缓存起来;
- 事件的销毁 - 释放内存
addEventListener
要有对应的removeEventListener
销毁setTimeout\setInterval
要有对应的clearTimeout\clearInterval
vue
长列表性能优化
Vue
会对数据进行劫持,实现双向数据绑定,但有的时候就说纯粹的数据展示,所以应避免vue
静态数据进行劫持,Object.freeze
方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
- 图片懒加载 以及 图片预加载的
- 用时才开始加载,不用不加载;图片懒加载适用于 大批量图片展示的场景;
- 提前加载下次所需图片,图片预加载能够使得用户在浏览时不会出现图片加载一半导致浏览不流畅的情况;
- 插件的按需引入以及取舍(你是否真的需要)
- 比如
UI
库, 往往都是比较大的,很占资源,应当使用按需引入; - 比如工具类
loadsh
, 往往你只是需要一个 防抖、节流,这时自己写一个也很简单,就没必要引入了;
- 服务端渲染 和 预渲染
- 服务端渲染
SSR (vue-server-renderer)
由服务端帮你渲染完成直接返回给浏览器,提高了用户体验,能快速的浏览的所需页面、SEO
友好;减轻了浏览器压力,但相应的造成了服务器的压力; - 预渲染
(prerender-spa-plugin)
利用了Puppeteer
的爬取页面的功能,在Webpack
构建阶段的最后,会本地启动一个Puppeteer
的服务,访问配置了预渲染的路由,然后将Puppeteer
中渲染的页面输出到 HTML 文件中,并建立路由对应的目录;
二、 react 方向
性能主要耗费在于update
阶段的diff
算法,因此性能优化也主要针对diff
算法
减少diff
算法触发次数(实际上减少update
流程的次数);
注: 父组件的render
必然会触发子组件进入update
阶段(无论props
是否更新)。
- 合并
setState
setState
机制是批更新策略,已经降低了update
过程的触发次数;- 尽量无论数据处理多么复杂,保证最后只调用一次
setState
, 合并setState
的调用;
memo
memo
会对state和prop进行浅比较控制是否刷新;memo
会缓存组件本身,站在全局的角度进行优化, 类似PureComponent、shouldComponentUpdate
- 适当使用
useCallback
useCallback
缓存的是一个函数,是对一个单独的props
值进行缓存, 返回上一次的函数引用, 可以保证依赖的值未发生改变的时候,不触发函数引用的改变;- 在向子组件传递函数
props
时,每次render
都会创建新函数,导致子组件不必要的渲染; useCallback
可以保证,无论render
多少次,我们的函数都是同一个函数,减小不断创建的开销;- 但是给所有钩子都用
useCallback
包裹, 是不对的,因为多数情况下无效,还导致代码可读性变差;需结合memo
配套使用;
useMemo
useMemo
缓存的是一个值,可以保证依赖的值未发生改变的时候,不触发值改变;- 会根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变;
- 会在渲染的时候执行, 不是渲染之后执行, 不建议有副作用相关的逻辑;
- 类组件
PureComponent / shouldComponentUpdate
shouldComponentUpdate
的返回值用于判断React
组件的输出是否受当前state
或props
更改的影响,当props
或state
发生变化时,shouldComponentUpdate
会在渲染执行之前被调用;PureComponent / shouldComponentUpdate
; 会进行props
和state
的浅比较来判断组件是否需要更新( 浅比较:只会比较到两个对象的ownProperty
是否符合Object.is(
) ,不会递归地去深层次比较);
- 慎用
forceUpdate
forceUpdate
会强制更新页面,直接进入componentWillUpdate
阶段,且无法拦截, 跳过优化手段(shouldComponentUpdate
), 直接进入render
; 建议少用;
- 正确使用 diff 算法 - 状态更新
- 不使用跨层级移动节点的操作
- 对于条件渲染多个节点时,尽量采用隐藏等方式切换节点,而不是替换节点;
- 尽量避免将后面的子节点移动到前面的操作,当节点数量较多时,会产生一定的性能问题;
- 其他
- 图片的
懒加载、预加载
、插件的按需引入
和vue
相同原理
三、 webpack / vite 方向
vite
自身已经做了很大程度的优化,所以主要的还是 webpack
方向;
- 生产环境关闭
sourceMap
SourceMap
建立错误-代码之间的映射,方便代码调试; 适用于开发阶段;SourceMap
占体积大头; 关闭之后你会发现项目小了很多;
- 对图片进行压缩
image-webpack-loader
插件
cdn
加载框架、插件资源
vue|react|UI框架|可视化插件
等;- 通过
cdn
的方式在script
标签中直接使用,减少打包体积,提高加载速度;
webpack-bundle-analyzer
构建结果分析
webpack-bundle-analyzer
打包后会生产一个本地服务,清楚的展示打包文件的包含关系和大小;- 比较大参照组件拆分思想,进行拆分;以及对应插件或者工具类按需导入;
vite
对应的是rollup-plugin-visualizer
插件, 功能类似
DllPlugin
提取公用库
- 开发过程中,我们经常需要引入大量第三方库,这些库并不需要随时修改或调试,我们可以使用
DllPlugin
和DllReferencePlugin
单独构建它们,配置webpack.dll.config.js
compression-webpack-plugin
开启gzip
压缩
- 开启
gzip
压缩可以有效压缩资源体积,压缩比率在3到10倍左右,可以大大节省服务器的网络带宽,提高资源获取的速度; - 压缩成功
Response Headers
中可以看到Content-Encoding: gzip
Nginx
配置如下
// nginx配置开启gzip压缩,nginx会根据配置情况对指定的类型文件进行压缩
gzip on; #开启或关闭gzip on off
gzip_disable "msie6"; #不使用gzip IE6
gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)
gzip_buffers 4 16k; #buffer 不用修改
gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; # 压缩文件类型
gzip_vary off;
Happypack
将loader
由单进程转为多进程
webpack
的缺点是单线程的,我们可以使用Happypack
把任务分解给多个子进程去并发执行,大大提升打包效率;- 配置的方法是把
loader
的配置转移到HappyPack
中去;
HardSourceWebpackPlugin
构建缓存
- 为模块提供中间缓存步骤, 能做到第二次打包速度倍数提升;
babel-loader
给loader
减轻负担
babel-loader
允许使用Babel
和webpack
转译JavaScript
文件;cacheDirectory
指定的目录将用来缓存loader
的执行结果。 后面的构建将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的Babel
重新编译过程,babel-loader
提速至少两倍;cache-loader
也能达到相同优化目的
babel-plugin-transform-runtime / tree-shaking
减少冗余代码
webpack2
默认已经支持tree-shaking
speed-measure-webpack-plugin
构建速度分析
- 很清楚的知道哪个模板构建花了多少秒, 针对性的优化
四、 Http 方向
DNS
预解析
- 通过
Html meta
标签来告知浏览器, 当前页面要做DNS
预解析;
<meta http-equiv="x-dns-prefetch-control" content="on" />
- 页面
header
中使用link
标签来强制对DNS预解析
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
- 使用
HTTP2
- 解析速度快
- 服务器解析
HTTP1.1
的请求时,必须不断地读入字节,直到遇到分隔符CRLF
为止。 - 而解析
HTTP2
的请求就不用这么麻烦,因为HTTP2
是基于帧的协议,每个帧都有表示帧长度的字段
- 服务器解析
- 多路复用
HTTP1.1
的Pipelining
技术会有阻塞的问题,处理请求响应是按照顺序的,也就是后发的请求有可能被先发的阻塞住;HTTP/2
的多路复用可以粗略的理解为非阻塞版的Pipelining
。即可以同时通过一个HTTP
连接发送多个请求,谁先响应就先处理谁,这样就充分的压榨了TCP
这个全双工管道的性能;
- 首部压缩
HTTP2
提供了首部压缩功能;HTTP/1.x
每次请求,都会携带大量冗余头信息,浪费了很多带宽资源;
- 减少
HTTP
请求数量
HTTP
会经历4 大步骤- 客户端连接到Web服务器
- 发送
HTTP
请求 - 服务器接受请求并返回
HTTP
响应 - 释放连接
TCP
链接;
HTTP
请求建立和释放需要时间, 并且随着网络情况而变化,网路差花费的时间将更长;- 不阻塞情况下
异步预先请求
或者合并请求
;
- 使用 http缓存
- 充分利用好
http缓存
能有效减轻服务器、浏览器压力(需要后台配合); - 原理:
http
缓存都是在第二次请求开始的,第一次服务器会在资源返回的响应中携带上四个常用的响应头,浏览器会通过判别这些响应值来决定资源缓存的状态,再次请求的时候浏览器会带上这些响应头;Cache-Control
(强缓存)- 可以携带多个响应值,这些值可以设置缓存时间、状态以及验证状态;
public
: 所有内容都将被缓存包括客户端、代理cdn节点private
: 只缓存到客户端,不缓存到代理服务器no-cache
: 需要先与服务器确认no-store
: 所有内容都不被缓存max-age
: 在多少秒之后失效
Expires
(强缓存)- 标记了数据的过期时间,超过其中规定的时间后,缓存会被定义为过期,优先级
Cache-Control
的max-age > Expires
- 标记了数据的过期时间,超过其中规定的时间后,缓存会被定义为过期,优先级
ETag
(协商缓存)--> 值是一个字符串(数据的哈希值),每个数据都有一个单独的标志- 浏览器会在后续的请求中携带上这个参数来确定缓存是否需要更新;
- 需要注意的是,
ETag
只有在本地缓存已过期(Expires
)或者缓存模式设置为no-cache(Cache-Control)
的时候,才会被浏览器携带上服务器端的值进行判别;
Last-Modified
(协商缓存)- 向浏览器发送一个数据上次被修改的时间;
- 浏览器就知道了该数据最后被修改的时间,后续请求中,会和服务器进行时间的比较,如果服务器上的时间比本地时间要新,说明数据有更改,浏览器需要重新下载数据;
- 缺点:当服务器响应中有
Expires
或者Cache-Control
设置了max-age
响应头的时候,浏览器不会向服务器发起校验请求,而是直接复用本地缓存。如果此时服务器进行了资源的更新,用户就无法获取到最新的资源,只能通过强制刷新浏览器缓存来跟服务器请求最新的资源
- 缓存的位置按照获取资源请求优先级,缓存位置依次如下:
Memory Cache
(内存缓存)
是浏览器最先尝试命中的缓存,也是响应最快的缓存。但是存活时间最短的,当进程结束后,tab 标签关闭后,缓存就不存在了,因为内存空间比较小,通常较小的资源放在内存缓存中,比如 base64 图片等资源Service Worker
(离线缓存)
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。Disk Cache
(磁盘缓存)
内存的优先性,导致大文件不能缓存到内存中,那么磁盘缓存则不同。虽然存储效率比内存缓存慢,但是存储容量和存储市场有优势。- Push Cache(推送缓存)它是最后一道缓存
- 优先使用get请求
get
请求不需要预检和交互; 频繁刷新浏览器不会对浏览器、服务器造成太大的压力,无伤;
- 使用
CDN
服务器端缓存加快访问速度 (俗称边缘计算)
- 原理:
CDN
网络是在用户和服务器之间增加了一层缓存层,将用户的请求引导到最优的缓存节点就近获取所需要的内容而不是服务器源站,从而降低网络用塞、加块访问速度响应用户的请求 (俗称负载均衡); - 过程:先向
CDN
边缘节点发起请求 -> 检测是否过期 -> 没有直接返回 -> 过期则去根服务器获取数据再返回; - 设置:通过
http
响应头中的Cache-Control
和max-age
的字段来设置CDN
边缘节点的数据缓存时间; - 例子:网站中大量的
css,html,js
等文件、大文件的下载(图片、视频、音频等),将这些静态内容推送到CDN
节点; - 构成:初始服务器,分布于各个节点的缓存服务器,重定向DNS服务器和内容交换服务器
- 主要技术:
-
- 负载均衡;
-
- 内容存储技术 (内容源的存储、内容在
cache
节点中的分布式存储);
- 内容存储技术 (内容源的存储、内容在
-
- 内容分发技术(构建网络,将链接到
IP
网络上的内容,快速的传输到用户终端)
- 内容分发技术(构建网络,将链接到
-
- 缺点:当源服务器资源更新后,如果
CDN
节点上缓存数据还未过期,用户访问到的依旧是过期的缓存资源,这会导致用户最终访问出现偏差。因此,开发者需要手动刷新相关资源,使CDN
缓存保持为最新的状态
- 减少
DNS
查找次数
- DNS用于映射主机名和
IP
地址,DNS
解析有代价,一般一次解析需要20~120
毫秒。浏览器在DNS
查询完成前不会下载任何东西,所以浏览器会想办法对DNS的查找结果进行缓存 - 减少域名主机可减少DNS查询的次数,最理想的方法就是将所有的内容资源都放在同一个域(
Domain
)下面,这样访问整个网站就只需要进行一次DNS查找,这样可以提高性能。 - 在
HTTP /1.1
中放在同个域下面会带来一定数量的并行度(它的建议是2
),那么就会出现下载资源时的排队现象,这样就会降低性能,推荐客户端针对每个域在一个网站里面使用至少2
个域,但不多于4
个域
- 请求返回体的压缩、分页、缓存
- 当一个请求返回数据比较多(如数据字典、图片、execl表格、pdf、音频、视频)时;优先和后台协商使用
缓存、分页、切片
等方案; - 并且
异步请求
放到请求队尾
, 当接收数据很大时,你会发现浏览器会进入卡死状态;
四、 图片方向
- 优先使用
雪碧图
- 图片、图标的切换优先使用雪碧图代替、以减少体积, 提高响应速度;
-
使用
font
字体、svg、base64、JPG、JPEG、WEBP
格式的图片 -
列表图片使用预加载、懒加载、及脱离文档流后进行
DOM
回收
- 大批量图片渲染是很耗浏览器性能的,并且会阻塞其他渲染,这时肯定需要使用预加载、懒加载方法;
- 对图片进行操作时,尽量脱离文档流后进行
DOM
回收,避免重绘重排;
- 使用
http、cdn
缓存、不失帧情况下对图片压缩
- 缓存、压缩目的都是快速响应用户操作显示图片.
五、浏览器渲染方向
SSR
(优化首页渲染时间)、骨架屏、开启gzip
压缩、js
混淆(无效字符及注释的删除、码语义的缩减和优化)css
的文件放在头部、css
压缩、合并css
资源- 减少重定向、减少外链、不滥用
web
字体、,js文件放在尾部或者异步(async
和defer
、动态脚本创建) ( 标签preload
渲染前加载,prefetch,dns-prefetch
渲染完成后空闲时间加载 ) - 避免內联样式、避免
html
里执行js
, 多次修改样式、结构,尽量合并在一起修改; - 使用
css
动画、减少css
表达式、使用requestAnimationFrame
操作动画 - 避免重绘重排、减少
DOM
元素个数 - 批量操作
DOM
,脱离文档流后在操作; - 使用
css3 GPU
硬件加速
translate3d、translateZ、rotate、scale、transform、opacity、filters
等动画效果不会引起回流重绘
- 对于频繁操作使用节流、防抖
- 节流:短时间内大量触发同一事件,在函数执行一次之后,该函数在指定的时间期限内不再执行,直至过了这段时间才重新生效(例如监听滚动条
scroll
事件); - 防抖:如果短时间内大量触发同一事件,只会执行一次函数(例如:
input
事件);
- 长列表优化
vue|raect
对应的UI
框架基本都提供了虚拟列表组件, 优先使用虚拟列表组件;
- 使用
JSON
格式
JSON
是一种轻量级的数据交换格式,是理想的数据交换格式。同时,JSON
是JavaScript
原生格式,这意味着在JavaScript
中处理JSON
数据不需要任何特殊的API
或工具包
- 控制
Cookie
大小和污染
Cookie
是本地的磁盘文件,每次浏览器都会去读取相应的Cookie
,所以建议去除不必要的Coockie
,使Coockie
体积尽量小- 使用
Cookie
跨域操作时注意在适应级别的域名上设置coockie
以便使子域名不受其影响 Cookie
是有生命周期的,所以请注意设置合理的过期时间,合理地Expire
时间和不要过早去清除coockie
- 其他
- 避免
404
、减少<link>
代替@import
、保持单个内容小于25K
六、 Chrome Performance 分析
多使用 Chrome Performance
的火焰图 查找性能瓶颈,针对性的优化;
- 评测报告中
FP、FCP、FMP、LCP、TTI、TTFB、FCI、FID、DCL、Speed Index
FP
"首次绘制" 是第一个“时间点”,它代表浏览器第一次向屏幕传输像素的时间,就是页面在屏幕上首次发生视觉变化的时间。FCP
"首次内容绘制", 代表浏览器第一次向屏幕绘制 “内容” (只有首次绘制文本、图片(包含背景图)、非白色的canvas
或SVG
时才被算作FCP
)FP
和FCP
可能是相同的时间,也可能是先FP
后FCP
。FMP
"首次有效绘制" 主要内容”开始出现在屏幕上的时间点。它是我们测量用户加载体验的主要指标LCP
可视区“内容”最大的可见元素开始出现在屏幕上的时间点。TTI
"可交互时间" 网页第一次 完全达到可交互状态 的时间点TTFB
表示浏览器接收第一个字节的时间FCI
告诉我们页面什么时候完全达到可用FID
FID指的是用户首次与产品进行交互时,我们产品可以在多长时间给出反馈DCL
DomContentloaded
事件触发的时间Speed Index
页面可见部分的平均时间- 商城、官网、博客这种页面更侧重FMP(用户希望尽快看到有价值的内容),而类似后台管理系统或在线PPT这种产品则更侧重
TTI
(用户希望尽快与产品进行交互)。
- 白屏时间计算
- 将代码脚本放在
</head>
前面就能获取白屏时间:
<script>
new Date().getTime() - performance.timing.navigationStart
</script>
- 首屏时间计算
- 在
window.onload
事件中执行以下代码,可以获取首屏时间:
new Date().getTime() - performance.timing.navigationStart
部分参考: