性能优化

构建相关

路由懒加载

最主要在于降低首屏加载资源大小,仅加载所需的页面资源文件,加快页面的显示

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) => { /* do */ })

但建议谨慎采取此方式

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: {},
// or: manualChunks: (id) => id,
},
},
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 的样式简化,可以使用 TailwindCSSUnoCSS 等方案

使用防抖节流

防抖,使得指定函数至少间隔 n 秒才会执行一次

节流,使得指定函数在 n 秒中最多执行一次

对于容易连续触发的事件,如 mousemovepointermovescrolltouchmovewheelresize 等,通过将事件处理方法绑定为防抖节流版本的方法,避免持续多次重复执行方法

使用 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-pluginvite-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 信息

启用 Keep-Alive

通过给请求头或响应头设置 Keep-Alive 头,通常用于提示连接超时时间和最大请求量

1
2
Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000

减少预检请求发起

可以在跨域请求设置 Access-Control-Max-Age 响应头指定预检请求的缓存期限,从而在指定期限内的跨域请求无需进行预检请求可以直接发起请求

有效期 10min
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 脚本加载的优先级,优先加载级别高的脚本,延后加载级别低的脚本

资源预加载

需要避免 preloadprefetch 的混用,以避免不必要的二次自由加载

  • 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-prefetchpreconnect 都是配对使用

但不建议过度使用 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 事件,可以通过调用返回的 FetchEventrespondWith() 方法自定义响应

图片字体相关

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(/* @type {1 - 10} */ 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
1
const A = 2 ** 8
good
1
2
const A = 2 << 3
const isPowerOfTwo = n => (n > 0) && (n & (n - 1) === 0)
发布于

2023-09-18

更新于

2023-10-24

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...