性能优化
构建相关
路由懒加载
最主要在于降低首屏加载资源大小,仅加载所需的页面资源文件,加快页面的显示
1 | import C from 'c' |
1 | { |
原理即将导航中的路由组件从静态 import
导入改为动态 import()
导入
组件懒加载
原理同路由懒加载
1 | import C from 'c' |
1 | const C = () => import('c') |
做组件懒加载一般在某些特别条件下使用,如组件仅在特定条件下才展示、当前页面文件过大、组件复用性较强
外部依赖懒加载
较大外部依赖可动态导入
1 | import * as THREE from 'three' |
1 | import('three').then((THREE) => { /* do */ }) |
但建议谨慎采取此方式
Tree Shaking 和 SideEffects
依赖 ESM 的静态特性,进行静态分析,在生成产物中去除无用的模块或代码,从而降低生成产物的大小
webpack 默认在构建阶段会启用 Tree Shaking,在开发阶段需手动配置
1 | module.exports = { |
1 | module.exports = { |
使用副作用
1 | { |
某些情况下需手动标记 /*#__PURE__*/
以标记代码,以标记语句是可执行 Tree Shaking 的
vite 原生基于 rollup 支持在构建阶段启用 Tree Shaking
构建产物压缩
webpack
可以使用 terser-webpack-plugin
插件来执行代码构建产物的压缩
1 | const TerserPlugin = require("terser-webpack-plugin") |
vite
内部默认集成 esbuild
进行代码构建产物的压缩,同时支持配置为使用 terser
来执行压缩
1 | export default { |
静态资源构建产物(特别是图片)的压缩可以使用一些插件实现,如 compression-webpack-plugin
等
外部库按需加载
外部库(特别是 UI 组件库)使用插件(如 babel-plugin-import
等)进行按需加载
代码分割
可以适当进行代码分割,避免一次性加载过大的资源文件,阻碍页面的展示;也需要避免过度分割,一次性执行过多的资源获取请求
1 | export default { |
内联代码文件
部分小体量的 JS 文件或 CSS 文件,可以内联到 HTML 文件中,减少请求的数量
分析外部依赖
可通过 webpack-bundle-analyzer
插件或 rollup-plugin-visualizer
插件来分析查看
1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') |
1 | import { visualizer } from 'rollup-plugin-visualizer' |
检查项目的依赖包是否有重复引用的情况,避免出现使用同样名称不同版本的依赖包引用的情况
渲染相关
骨架屏
主要应用于缩短白屏时长,特别是 SPA 单页应用
原理是直接把展示骨架屏的内容放在 html 文件内,在真正内容加载完后再隐藏骨架屏的内容
虚拟滚动
只渲染可视区域的列表项,非可见区域的不渲染
原理为计算列表的总高度,并在触发滚动事件时根据滚动高度更新起始下标和结束下标,从而取出相应的数据渲染元素
Worker 长任务优化
将一些长任务逻辑移入到 Worker 中,避免长任务的执行阻碍 UI 渲染而影响用户体验
是否使用 Worker,需要比较 Worker 通信时长与运算时长相比是否具有足够的优势
利用 requestAnimationFrame 周期任务
可以利用 requestAnimationFrame 处理周期任务
特别是需要较严格固定周期频率执行的情况(setInterval 和 setTimeout 无法保证准确的时间间隔)
同时 requestAnimationFrame 支持在页面隐藏或最小化时暂停执行周期任务,以节省性能(setInterval 和 setTimeout 不会因页面隐藏或最小化等因素暂停执行)
使用 CSS 动画过渡变换替代 JS
CSS 动画 Animation、过渡 Transition、变换 Transform 相较于 JS 性能通过上更具优势,并且浏览器更易于针对性地做优化
简化 CSS 选择器
避用通配符选择器 *
减少使用标签选择器
优先使用默认的样式继承
避免层数过大的选择器
代码复用及代码封装
提升代码复用率,进行功能代码封装等
CSS 的样式简化,可以使用 TailwindCSS
、UnoCSS
等方案
使用防抖节流
防抖,使得指定函数至少间隔 n 秒才会执行一次
节流,使得指定函数在 n 秒中最多执行一次
对于容易连续触发的事件,如 mousemove
、pointermove
、scroll
、touchmove
、wheel
、resize
等,通过将事件处理方法绑定为防抖节流版本的方法,避免持续多次重复执行方法
使用 will-change
优化动态效果
预先将执行动画的元素设置 will-change
CSS 属性,以便浏览器引擎将其视为单独图层来进行优化
注意点是,避免过度应用 will-change
属性;建议仅在需要时候 JS 动态设置该属性
减少页面重排重绘
重绘指元素的非几何样式改变引起的浏览器重新渲染目标元素的现象
重绘指元素的几何样式改变引起的浏览器重新渲染整个渲染树或的现象
使用 GPU 渲染
CSS中可使用如下一些方式将目标元素独立为合成层,从而进行独立渲染,以触发 GPU 渲染
- 指定
will-change
属性 - 3D 或者透视变换
perspective
transform
- 使用加速视频解码的 video 元素
- 拥有 3D 上下文(WebGL)或者加速 2D 上下文的 canvas 元素
- 使用 opacity、filter 实现 CSS 动画或使用一个动画 webkit 变换的元素
- 拥有加速 CSS 过滤器的元素
- 元素有一个包含复合层的后代节点
- 元素有一个兄弟元素在复合图层渲染,且具备较低的 z-index
避免无效请求
避免出现无效请求,例如表单提交频繁点击的问题,或路由切换时还有未完成的请求;对于服务器和用户来说,会造成不必要的困扰
网络相关
使用 HTTP2
HTTP2 支持头部压缩,能够减少数据传输量,节省消息投占用的网络的流量
且 HTTP2 支持多路复用、服务器推送等功能
gzip 压缩
HTTP 头部及资源启用 gzip 压缩,能够大大减少网络传输的数据量
可以使用 compression-webpack-plugin
或 vite-plugin-compression
压缩打包资源至 gzip
1 | const compression = require('compression-webpack-plugin') |
1 | import compression from 'vite-plugin-compression' |
对资源服务开启 gzip 支持
1 | gzip on; |
一般推荐提前处理完成 gzip 文件,再直接交由 nginx 服务
Cookie 简化
执行网络请求时,浏览器会自动带上同源的 Cookie 信息,一定程度上会增大请求头的大小
可以通过精简 Cookie 的内容,来降低请求信息的大小
同时可以对静态资源单独部署,避免请求携带不必要的 Cookie 信息
启用 Keep-Alive
通过给请求头或响应头设置 Keep-Alive 头,通常用于提示连接超时时间和最大请求量
1 | Connection: Keep-Alive |
减少预检请求发起
可以在跨域请求设置 Access-Control-Max-Age
响应头指定预检请求的缓存期限,从而在指定期限内的跨域请求无需进行预检请求可以直接发起请求
1 | Access-Control-Max-Age: 600 |
资源相关
script 加载方式
- 正常模式
JS 会阻碍 DOM 渲染
<script src="main.js"></script>
async
模式
异步加载 JS,执行无顺序,加载完成后立即执行
可以用于加载与 DOM 无关的 JS 资源,如埋点统计等
<script async src="main.js"></script>
defer
模式
异步加载 JS,执行有顺序,加载完成后统一在 DOMContentLoaded 事件触发前执行
一般情况均可使用 defer 优化 JS 资源的加载,避免 JS 脚本加载与执行阻碍网页的渲染
<script defer src="main.js"></script>
module
模式
行为上会类似于 defer 模式
<script type="module" src="main.js"></script>
fetchpriority
资源加载优先级
可以利用 fetchpriority
HTML 属性指定 script 脚本加载的优先级,优先加载级别高的脚本,延后加载级别低的脚本
资源预加载
需要避免 preload
和 prefetch
的混用,以避免不必要的二次自由加载
preload
预先下载当前页面将使用的资源并缓存(不会执行),会提升资源的优先级
需同时指定 as
属性与 href
属性
<link rel="preload" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />
建议指定 type
属性,以避免浏览器下载格式不支持的资源
建议同时指定 crossorigin
属性
prefetch
预加载未来页面将使用的资源,并保存在缓存内一段时间,会降低资源的优先级
要求当前页面需为安全上下文
<link rel="prefetch" href="main.js" />
modulepreload
类似于 preload
预加载当前页面将使用的模块脚本资源,并进行解析与执行
<link rel="modulepreload" href="main.js" />
prerender
预加载目标资源并提前在后台处理执行
仅部分浏览器支持该非标准特性
网络预连接
一般情况下,dns-prefetch
与 preconnect
都是配对使用
但不建议过度使用 preconnect
,仅用于未来一段时间极可能访问或请求的 origin;否则仅应用 dns-prefetch
同时 dns-prefetch
的浏览器兼容性优于 preconnect
建议使用以上两属性的同时指定 crossorigin
属性
dns-prefetch
提前执行目标 origin 的 DNS 解析,可以加快未来将访问或请求的 origin 的处理速度(直接使用已预先解析的 DNS 缓存)
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />
preconnect
提前执行目标 origin 的连接 —— DNS 解析、TCP 连接(及 TLS 握手),可以加快未来将访问或请求的 origin 的处理速度
<link rel="preconnect" href="https://fonts.googleapis.com/" />
避用外部依赖
尽量减少对非必要的外部依赖的使用,使用轻量级别替代方案或者自行实现
常见的如:
使用轻量级 day.js
替代 moment.js
使用 ESM 的 lodash-es
替代 CJS 的 lodash
使用 CDN 服务静态资源
CDN 即 Content Delivery Network,其具有分布于多个地域的服务器阵列
CDN 可以降低私有服务器的访问压力
地理位置的距离可能相对更近,一定程度上可以降低网络资源加载的时延
CDN 保证了比较正确的缓存配置
合理配置缓存策略
服务器在响应资源时,通过指定 Expires
响应头或 Cache-Control
响应头来控制浏览器该资源的缓存策略;若被指定为强缓存并且在有效期内直接使用缓存;反之若为被禁止使用缓存,则进行协商缓存,通过 If-Modified-Since
头向服务器提供浏览器缓存的资源的修改时间(在获取资源时服务器通过 Last-Modified
头指定)或通过 If-None-Match
头向服务器提供浏览器缓存的资源的标识符(在获取资源时服务器通过 ETag
头指定)
Expires
(推荐使用Cache-Control
代替,其优先级更高)【响应头】指定缓存失效的时间
Cache-Control
【响应头,请求头】配置缓存的策略及有效期
no-store
不允许缓存no-cache
允许缓存,但使用前需进行服务端验证must-revalidate
允许缓存,有效期内直接使用缓存,超出有效期需进行服务端验证,通常结合 max-age=N 使用max-age=N
指定缓存的有效期
Last-Modified
【响应头】/If-Modified-Since
【请求头】资源最近修改时间
ETag
【响应头】/If-None-Match
【请求头】资源唯一标识符
ServiceWorker
实现可控缓存
利用 ServiceWorker 结合 CacheStorage 实现可控缓存,原理是基于受 ServiceWorker 控制的上下文会在 ServiceWorker 全局触发 fetch
事件,可以通过调用返回的 FetchEvent
的 respondWith()
方法自定义响应
图片字体相关
webp
图片
webp 格式图片大小通常比同等情况下的其他格式图片大小有较大优势,因此若浏览器支持 webp 格式图片,优先使用 webp 格式图片
可以利用离线或在线 webp 图片格式转换工具转换图片格式为 webp
图片懒加载
JS 手动控制
初始不指定图片标签的
src
属性,直到图片需要展示时再指定其src
属性,避免图片的自动预加载1
<img data-src="/img/png" />
1
2
3
4// 适当情况下调用该方法
function loadImg(el) {
el.src = el.getAttribute('data-src')
}利用 img 标签特性(更推荐)
可以设置
img
标签的loading
属性实现懒加载功能,将属性值指定为lazy
以惰性加载图片同时可以指定
img
标签的fetchpriority
属性,以控制获取图片资源的优先级,设定为high
以提升获取的优先级,设定为low
以降低获取的优先级同时可以指定
img
标签的decoding
属性,以设定解码图片的模式(是否允许在图片解码完成前展示图片),设定为sync
以同步解码图片,设定为async
以异步解码图片1
<img src="/img/png" alt="" loading="lazy" fetchpriority="auto" decoding="auto" />
字体图标
将小图标利用字体形式加载,如 IconFont
1 | @import url('//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css'); |
1 | <i class="iconfont icon-xxx"></i> |
通常加载资源大小会更小,并且能够避免重复加载图片并降低请求数量,且支持修改各类字体样式
内联图片
将小图片转换为 base64 编码内联入 html 文档,可以减少请求数量
webpack 中可以使用 url-loader
插件自动转换内联图片
1 | module.exports = { |
vite 原生支持内联图片,默认在图片大小小于 4KB 时启用
可以通过 build.assetsInlineLimit
选项配置启用的阈值
1 | export default { |
图片裁剪
对图片生成多个尺寸的备用图片,使用时根据需要加载不同尺寸的图片,减少不必要的资源流量
图片单独部署
将图片等静态资源部署在单独的静态资源服务器或是 CDN 上,避免直接打包到项目中
图片尺寸指定
设置图片标签的尺寸大小,防止图片加载中导致页面布局抖动,影响 CLS 指标
字体按需生成
使用第三方字体库时,尽可能按需生成,避免不必要的全量引入字体库
代码相关
JSON 字符串使用
对于大对象数据,尽量采用 JSON 格式而非 JS 对象格式,因为 JSON 语法比 JS 简单,解析速度更快
如 vite 支持将 JSON 文件打包为 JSON 字符串而非 JS 对象
1 | export default { |
if 逻辑提前跳出
提前结束的逻辑利于编译器的优化
1 | function () { |
1 | function () { |
逻辑能提前结束就提前结束
switch 连续值优化
switch 对于连续值会处理成数组,而数组会具有更高效率的随机访问性能
1 | function get(/* @type {1 - 10} */ level) { |
1 | function getPrice(level) { |
若条件可以处理成连续的数字,可以使用 switch 来进行优化
循环减少执行次数
对于循环,满足条件或完成任务后即刻跳出,避免不必要的执行损耗
1 | function find(data) { |
1 | function find(data) { |
提取集合数组长度
在循环处理数组、集合等容器的元素,若可以保证容器的容量不会发生变化,可以提前提取容器的容量,避免在循环中重复获取
适当使用位运算
对于一些和 2 相关的乘除法或者条件相关的,可以用位运算替代
1 | const A = 2 ** 8 |
1 | const A = 2 << 3 |