一、性能问题分析
1.1 问题背景
某用vite
构建的SPA
系统在实现离线大文件的下载功能后,多个页面出现首屏加载缓慢问题:
- 使用该文件的页面:首次加载10-15秒
- 连带影响:未使用该功能的页面也受到波及
特别影响用户体验感和开发速度。
1.2 分析工具详解
(1)VITE项目-Rollup打包分析(rollup-plugin-visualizer)
(上图来自github)
github地址:rollup-plugin-visualizer
简介ℹ️:vite插件,可视化的打包依赖分析工具。通过分析依赖模块的大小占比,可以让我们更有针对性的进行体积优化。
配置示例:
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
// visualizer构建分析,注意放在最后面,打包完成后分析
visualizer({
open: true, // 自动打开
gzipSize: true,
template: 'treemap' // 树状图模式
filename: 'build-stats.html',
})
]
}
使用场景:
- 识别打包产物中的巨型模块
- 对比优化前后的体积变化
(2)Chrome DevTools实战技巧
Network面板进阶用法:
1. 切换至All查看所有资源请求情况
2. Size,排序资源,定位体积较大的文件
3. Time, 点击顶部排序可找出加载时间较长的部分
4. Initiator,可进一步查看文件引入路径
Coverage工具:
1. Ctrl+Shift+P -> Show Coverage
2. 刷新页面获取代码使用率
3. 红色标注未使用代码段
二、优化方案实现
2.1 静态资源压缩(核心方案)
(1)依赖清理
原理:Tree Shaking + 按需加载
实现:
// 原始代码(全量引入)
import { HeavyLib } from 'univerjs';
// 优化代码(按需加载)
import { SheetRenderer } from 'univerjs/core';
效果:移除univerjs引入,体积减少10MB+
(2)i18n词条过滤
实现原理:
通过构建时脚本过滤仅保留相关词条
关键代码:
// filter-i18n.js
const prefix = 'report.';
const filtered = Object.keys(fullLang)
.filter(k => k.startsWith(prefix))
.reduce((acc, k) => (acc[k] = fullLang[k], acc), {});
fs.writeFileSync('report-lang.json', JSON.stringify(filtered));
构建命令:
# 自定义构建脚本
pnpm update:my-i18n
(3)字体优化
实现方案:
/* 移除定制字体 */
/* 原代码:@font-face { font-family: CustomFont; ... } */
/* 使用系统字体 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui;
}
antd可结合ConfigProvider进行局部配置,以免影响整体的字体。
核心代码:
import React from 'react';
import { ConfigProvider } from 'antd';
const App: React.FC = () => (
<ConfigProvider
theme={{ token: { fontFamily:'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,Noto Color Emoji' } }}>
<MyApp />
</ConfigProvider>
);
export default App;
(4)vite-build.rollupOptions.external 移除不用的依赖
结合vite配置中的配置项,可以主动排除不想编译的比较大的依赖和文件;不过仅能是排除真的不需要用的部分。
2.2 动态加载策略, 关键渲染路径优化
(1)React异步组件: 懒加载+代码分割
实现原理:代码分割 + 请求延迟((React.lazy + Suspense))
关键代码:
const ReportViewer = React.lazy(() => import('./ReportViewer'));
function App() {
return (
<Suspense fallback={<Spin />}>
<ReportViewer />
</Suspense>
);
}
优化效果 有大依赖项的菜单,补充设置懒加载,各路由按需引入;index.js主入口文件体积缩小4.9m。
(2)useSuspenseQuery + import(),实现大模块局部延迟加载
实现原理:等首屏加载完成后灵活控制加载时机,再异步加载大模块;
- 页面渲染后,react-query 会在后台异步加载大模块
- 不会阻塞首屏渲染
- 灵活控制加载时机(比如打开弹窗时、页面空闲时、用户滑动到某区域时等)
- 结合 Suspense,loading体验更好
关键代码:
import { useSuspenseQuery } from '@tanstack/react-query'
import { t } from 'i18next'
import { Suspense } from 'react'
async function loadDownloadBtn() {
const mod = await import('@/pages/xxxx')
return mod.default
}
function useDownloadBtn() {
return useSuspenseQuery({
queryKey: ['DownloadBtn'],
queryFn: loadDownloadBtn,
refetchOnMount: false,
}).data
}
function DownloadBtnWrapper(props) {
const DownloadBtn = useDownloadBtn()
return <DownloadBtn {...props} />
}
export function DownloadBtnSuspenseWrapper(props: any) {
return (
<Suspense
fallback={
<span className='cursor-not-allowed text-disabled-text-gray'>
{t('global.downloadLoadingMsg')}
</span>
}
>
<DownloadBtnWrapper {...props} />
</Suspense>
)
}
(3)用fetch方法直接从public目录获取静态大文件📃
实现原理:
- 不需要React组件生命周期
- 灵活控制加载时机,实现类似于发请求从后端获取的效果~
- 另外,通过将大文件移入public目录下,以免整个项目build时被编译,以提升构建速度
核心代码:
export const fetchStaticFileRaw = async () => {
try {
const response = await fetch('/bigFile.html?t=' + t)
const htmlStr = await response.text()
return htmlStr
} catch (error) {
message.error('Failed to fetch big file')
return ''
}
}
三、优化效果展示
指标 | 优化前 | 优化后 | 提升幅度 |
---|---|---|---|
大文件体积 | 39.7MB | 3.76MB | ↓35.94MB (90.5%) |
首屏JS体积 | 41MB | 6.7MB | ↓34.52MB (83.6%) |
LCP时间 | 10.47s | 1.53s | ↓8.94s (85.4%) |
资源加载峰值 | >5s | <1s | ↓4.1s (82%) |
四、编码思考与经验沉淀
-
关键优化手段:
- 精准依赖管理:通过静态分析剔除冗余代码(如UniverJS、Redux),vite结合按需加载(i18n词条过滤)减少死代码。
- 动态加载策略:利用React.lazy + Suspense + React-Query / fetch方法实现按需加载,避免阻塞关键渲染路径。
- 工程化定制:通过Vite插件链(如
rollup-plugin-visualizer
)和自定义脚本(词条过滤命令行工具)实现自动化优化。
-
“代价与收益”的权衡:
- 动态加载可能增加代码复杂度,需通过封装(如
DownloadBtnSuspenseWrapper
)保证可维护性。
- 动态加载可能增加代码复杂度,需通过封装(如
-
依赖治理的重要性:
- 第三方库(如UniverJS)的引入需严格评估必要性,避免“大而全”的依赖污染打包结果。
- 建立代码体积监控机制(如定期运行
visualizer
),防止体积劣化。
-
跨角色协作的价值:
- 与UX设计师和产品沟通替换字体资源,验证设计可行性。
性能优化不是一次性任务,而是持续改进的过程。建议将本文方案融入日常开发流程,通过工具化和自动化实现长效治理。