构建相关 路由懒加载 最主要在于降低首屏加载资源大小,仅加载所需的页面资源文件,加快页面的显示
bad 1 2 3 4 5 import C from 'c' { component : C, }
good 1 2 3 { component : () => import ('c' ), }
原理即将导航中的路由组件从静态 import
导入改为动态 import()
导入
组件懒加载 原理同路由懒加载
worse 1 2 3 4 5 6 7 import C from 'c' export default { components : { C, }, }
better 1 2 3 4 5 6 7 const C = ( ) => import ('c' )export default { components : { C, }, }
做组件懒加载一般在某些特别条件下使用,如组件仅在特定条件下才展示、当前页面文件过大、组件复用性较强
外部依赖懒加载 较大外部依赖可动态导入
worse 1 import * as THREE from 'three'
better 1 import ('three' ).then ((THREE ) => { })
但建议谨慎采取此方式
Tree Shaking 和 SideEffects 依赖 ESM 的静态特性,进行静态分析,在生成产物中去除无用的模块或代码,从而降低生成产物的大小
webpack 默认在构建阶段会启用 Tree Shaking,在开发阶段需手动配置
webpack.config.js 1 2 3 4 5 6 module .exports = { mode : 'development' , optimization : { usedExports : true , }, }
webpack.config.js 1 2 3 module .exports = { mode : 'production' , }
使用副作用
package.json 1 2 3 { "sideEffects" : false }
某些情况下需手动标记 /*#__PURE__*/
以标记代码,以标记语句是可执行 Tree Shaking 的
vite 原生基于 rollup 支持在构建阶段启用 Tree Shaking
构建产物压缩 webpack
可以使用 terser-webpack-plugin
插件来执行代码构建产物的压缩
webpack.config.js 1 2 3 4 5 6 7 8 const TerserPlugin = require ("terser-webpack-plugin" )module .exports = { optimization : { minimize : true , minimizer : [new TerserPlugin ()], }, }
vite
内部默认集成 esbuild
进行代码构建产物的压缩,同时支持配置为使用 terser
来执行压缩
vite.config.js 1 2 3 4 5 6 7 8 9 10 11 export default { esbuild : {}, build : { cssMinify : 'esbuild' , minify : 'esbuild' , terserOptions : {}, }, optimizeDeps : { esbuildOptions : {}, }, }
静态资源构建产物(特别是图片)的压缩可以使用一些插件实现,如 compression-webpack-plugin
等
外部库按需加载 外部库(特别是 UI 组件库)使用插件(如 babel-plugin-import
等)进行按需加载
代码分割 可以适当进行代码分割,避免一次性加载过大的资源文件,阻碍页面的展示;也需要避免过度分割,一次性执行过多的资源获取请求
vite.config.js 1 2 3 4 5 6 7 8 9 10 11 export default { build : { rollupOptions : { output : { manualChunks : {}, }, }, cssCodeSplit : {}, }, }
内联代码文件 部分小体量的 JS 文件或 CSS 文件,可以内联到 HTML 文件中,减少请求的数量
分析外部依赖 可通过 webpack-bundle-analyzer
插件或 rollup-plugin-visualizer
插件来分析查看
webpack.config.js 1 2 3 4 5 6 7 const { BundleAnalyzerPlugin } = require ('webpack-bundle-analyzer' )module .exports = { plugins : [ new BundleAnalyzerPlugin (), ], }
vite.config.js 1 2 3 4 5 6 7 import { visualizer } from 'rollup-plugin-visualizer' export default { plugins : [ 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
webpack.config.js 1 2 3 4 5 const compression = require ('compression-webpack-plugin' )module .exports = { plugins : [new compression ()], }
vite.config.js 1 2 3 4 5 import compression from 'vite-plugin-compression' export default { plugins : [compression ()] }
对资源服务开启 gzip 支持
nginx.conf 1 2 3 4 5 6 7 8 9 gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css; gzip_disable "MSIE [1-6]\."; gzip_proxied any; gzip_vary on;
一般推荐提前处理完成 gzip 文件,再直接交由 nginx 服务
Cookie 简化 执行网络请求时,浏览器会自动带上同源的 Cookie 信息,一定程度上会增大请求头的大小
可以通过精简 Cookie 的内容,来降低请求信息的大小
同时可以对静态资源单独部署,避免请求携带不必要的 Cookie 信息
启用 Keep-Alive 通过给请求头或响应头设置 Keep-Alive 头,通常用于提示连接超时时间和最大请求量
1 2 Connection : Keep-AliveKeep-Alive : timeout=5, max=1000
减少预检请求发起 可以在跨域请求设置 Access-Control-Max-Age
响应头指定预检请求的缓存期限,从而在指定期限内的跨域请求无需进行预检请求可以直接发起请求
有效期 10min 1 Access-Control-Max-Age : 600
资源相关 script 加载方式
JS 会阻碍 DOM 渲染
<script src="main.js"></script>
异步加载 JS,执行无顺序,加载完成后立即执行
可以用于加载与 DOM 无关的 JS 资源,如埋点统计等
<script async src="main.js"></script>
异步加载 JS,执行有顺序,加载完成后统一在 DOMContentLoaded 事件触发前执行
一般情况均可使用 defer 优化 JS 资源的加载,避免 JS 脚本加载与执行阻碍网页的渲染
<script defer src="main.js"></script>
行为上会类似于 defer 模式
<script type="module" src="main.js"></script>
可以利用 fetchpriority
HTML 属性指定 script 脚本加载的优先级,优先加载级别高的脚本,延后加载级别低的脚本
资源预加载 需要避免 preload
和 prefetch
的混用,以避免不必要的二次自由加载
预先下载当前页面将使用的资源并缓存(不会执行),会提升资源的优先级
需同时指定 as
属性与 href
属性
<link rel="preload" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />
建议指定 type
属性,以避免浏览器下载格式不支持的资源
建议同时指定 crossorigin
属性
预加载未来页面将使用的资源,并保存在缓存内一段时间,会降低资源的优先级
要求当前页面需为安全上下文
<link rel="prefetch" href="main.js" />
类似于 preload
预加载当前页面将使用的模块脚本资源,并进行解析与执行
<link rel="modulepreload" href="main.js" />
预加载目标资源并提前在后台处理执行
仅部分浏览器支持该非标准特性
网络预连接 一般情况下,dns-prefetch
与 preconnect
都是配对使用
但不建议过度使用 preconnect
,仅用于未来一段时间极可能访问或请求的 origin;否则仅应用 dns-prefetch
同时 dns-prefetch
的浏览器兼容性优于 preconnect
建议使用以上两属性的同时指定 crossorigin
属性
提前执行目标 origin 的 DNS 解析,可以加快未来将访问或请求的 origin 的处理速度(直接使用已预先解析的 DNS 缓存)
<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />
提前执行目标 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
【响应头,请求头】
配置缓存的策略及有效期
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
插件自动转换内联图片
webpack.config.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module .exports = { module : { rules : [ { test : /\.(png|jpg|gif)$/i , use : [ { loader : 'url-loader' , options : { limit : 1024 * 8 , }, }, ], }, ], }, }
vite 原生支持内联图片,默认在图片大小小于 4KB 时启用
可以通过 build.assetsInlineLimit
选项配置启用的阈值
vite.config.js 1 2 3 4 5 export default { build : { assetsInlineLimit : 4096 , } }
图片裁剪 对图片生成多个尺寸的备用图片,使用时根据需要加载不同尺寸的图片,减少不必要的资源流量
图片单独部署 将图片等静态资源部署在单独的静态资源服务器或是 CDN 上,避免直接打包到项目中
图片尺寸指定 设置图片标签的尺寸大小,防止图片加载中导致页面布局抖动,影响 CLS 指标
字体按需生成 使用第三方字体库时,尽可能按需生成,避免不必要的全量引入字体库
代码相关 JSON 字符串使用 对于大对象数据,尽量采用 JSON 格式而非 JS 对象格式,因为 JSON 语法比 JS 简单,解析速度更快
如 vite 支持将 JSON 文件打包为 JSON 字符串而非 JS 对象
vite.config.js 1 2 3 4 5 export default { json : { stringify : true , }, }
if 逻辑提前跳出 提前结束的逻辑利于编译器的优化
bad 1 2 3 4 5 6 7 8 9 10 11 function ( ) { if (A) { if (B) { return 'good' } else { return 'bad' } } else { return 'bad' } }
good 1 2 3 4 5 6 7 function ( ) { if (A && B) { return 'good' } return 'bad' }
逻辑能提前结束就提前结束
switch 连续值优化 switch 对于连续值会处理成数组,而数组会具有更高效率的随机访问性能
bad 1 2 3 4 5 6 7 function get ( level ) { if (level >= 10 ) return 100 if (level >= 9 ) return 80 if (level >= 6 ) return 50 if (level >= 1 ) return 20 return 10 }
good 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function getPrice (level ) { switch (level) { case 10 : return 100 case 9 : return 80 case 8 : case 7 : case 6 : return 50 case 5 : case 4 : case 3 : case 2 : case 1 : return 20 default : return 10 } }
若条件可以处理成连续的数字,可以使用 switch 来进行优化
循环减少执行次数 对于循环,满足条件或完成任务后即刻跳出,避免不必要的执行损耗
bad 1 2 3 4 5 6 7 8 9 function find (data ) { let result = null for (let i = 0 ; i < data.length ; i++) { if (data[i].key === KEY ) { result = data[i] } } return result }
good 1 2 3 4 5 6 7 function find (data ) { for (let i = 0 ; i < data.length ; i++) { if (data[i].key === KEY ) { return data[i] } } }
提取集合数组长度 在循环处理数组、集合等容器的元素,若可以保证容器的容量不会发生变化,可以提前提取容器的容量,避免在循环中重复获取
适当使用位运算 对于一些和 2 相关的乘除法或者条件相关的,可以用位运算替代
bad
good 1 2 const A = 2 << 3 const isPowerOfTwo = n => (n > 0 ) && (n & (n - 1 ) === 0 )